Files
ERitors-Scribe-Desktop/electron/database/models/World.ts
natreex e45a15225b Add enable/disable management for book tools (characters, worlds, and locations)
- Introduced toggling functionality for managing `characters`, `worlds`, and `locations` tool availability per book.
- Updated `CharacterComponent`, `WorldSetting`, and `LocationComponent` with toggle switches for tool enablement.
- Added `book_tools` database table and related schema migration for storing tool settings.
- Extended API calls, models, and IPC handlers to support tool enablement states.
- Localized new strings for English with supporting descriptions and messages.
- Adjusted conditional rendering logic across components to respect tool enablement.
2026-01-14 17:42:59 -05:00

279 lines
13 KiB
TypeScript

import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js";
import WorldRepository, { WorldElementValue, WorldQuery } from "../repositories/world.repository.js";
import BookRepo, {BookToolsTable} from "../repositories/book.repository.js";
export interface SyncedWorld {
id: string;
name: string;
lastUpdate: number;
elements: SyncedWorldElement[];
}
export interface SyncedWorldElement {
id: string;
name: string;
lastUpdate: number;
}
export interface WorldElement {
id: string;
name: string;
description: string;
type?: number;
}
export interface WorldProps {
id: string;
name: string;
history: string;
politics: string;
economy: string;
religion: string;
languages: string;
laws: WorldElement[];
biomes: WorldElement[];
issues: WorldElement[];
customs: WorldElement[];
kingdoms: WorldElement[];
climate: WorldElement[];
resources: WorldElement[];
wildlife: WorldElement[];
arts: WorldElement[];
ethnicGroups: WorldElement[];
socialClasses: WorldElement[];
importantCharacters: WorldElement[];
}
export interface WorldListResponse {
worlds: WorldProps[];
enabled: boolean;
}
/**
* Mapping of element type keys to their corresponding numeric type identifiers.
*/
const ELEMENT_TYPE_MAP: Record<string, number> = {
laws: 1,
biomes: 2,
issues: 3,
customs: 4,
kingdoms: 5,
climate: 6,
resources: 7,
wildlife: 8,
arts: 9,
ethnicGroups: 10,
socialClasses: 11,
importantCharacters: 12
};
/**
* Mapping of numeric type identifiers to their corresponding WorldProps keys.
*/
const ELEMENT_TYPE_KEYS: Record<number, keyof WorldProps> = {
1: 'laws',
2: 'biomes',
3: 'issues',
4: 'customs',
5: 'kingdoms',
6: 'climate',
7: 'resources',
8: 'wildlife',
9: 'arts',
10: 'ethnicGroups',
11: 'socialClasses',
12: 'importantCharacters'
};
export default class World {
/**
* Creates a new world for a book.
* @param userId - The unique identifier of the user creating the world
* @param bookId - The unique identifier of the book to associate the world with
* @param worldName - The name of the new world
* @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
* @param existingWorldId - Optional existing world ID for syncing purposes
* @returns The unique identifier of the newly created world
* @throws Error if a world with the same name already exists for this book
*/
public static addNewWorld(userId: string, bookId: string, worldName: string, lang: 'fr' | 'en' = 'fr', existingWorldId?: string): string {
const userEncryptionKey: string = getUserEncryptionKey(userId);
const hashedWorldName: string = System.hashElement(worldName);
if (!existingWorldId && WorldRepository.checkWorldExist(userId, bookId, hashedWorldName, lang)) {
throw new Error(lang === "fr" ? `Tu as déjà un monde ${worldName}.` : `You already have a world named ${worldName}.`);
}
const encryptedWorldName: string = System.encryptDataWithUserKey(worldName, userEncryptionKey);
const worldId: string = existingWorldId || System.createUniqueId();
return WorldRepository.insertNewWorld(worldId, userId, bookId, encryptedWorldName, hashedWorldName, lang);
}
/**
* Retrieves all worlds and their elements for a specific book.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
* @returns WorldListResponse containing an array of WorldProps and enabled flag
*/
public static getWorlds(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): WorldListResponse {
const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang);
const enabled: boolean = bookTools ? bookTools.worlds_enabled === 1 : false;
const worldQueryResults: WorldQuery[] = WorldRepository.fetchWorlds(userId, bookId, lang);
const userEncryptionKey: string = getUserEncryptionKey(userId);
const worlds: WorldProps[] = [];
for (const queryRow of worldQueryResults) {
const existingWorld: WorldProps | undefined = worlds.find((world: WorldProps) => world.id === queryRow.world_id);
if (!existingWorld) {
const newWorld: WorldProps = {
id: queryRow.world_id,
name: System.decryptDataWithUserKey(queryRow.world_name, userEncryptionKey),
history: queryRow.history ? System.decryptDataWithUserKey(queryRow.history, userEncryptionKey) : '',
politics: queryRow.politics ? System.decryptDataWithUserKey(queryRow.politics, userEncryptionKey) : '',
economy: queryRow.economy ? System.decryptDataWithUserKey(queryRow.economy, userEncryptionKey) : '',
religion: queryRow.religion ? System.decryptDataWithUserKey(queryRow.religion, userEncryptionKey) : '',
languages: queryRow.languages ? System.decryptDataWithUserKey(queryRow.languages, userEncryptionKey) : '',
laws: [],
biomes: [],
issues: [],
customs: [],
kingdoms: [],
climate: [],
resources: [],
wildlife: [],
arts: [],
ethnicGroups: [],
socialClasses: [],
importantCharacters: [],
};
worlds.push(newWorld);
if (queryRow.element_type) {
const worldElement: WorldElement = {
id: queryRow.element_id as string,
name: queryRow.element_name ? System.decryptDataWithUserKey(queryRow.element_name, userEncryptionKey) : '',
description: queryRow.element_description ? System.decryptDataWithUserKey(queryRow.element_description, userEncryptionKey) : ''
};
const elementKey: keyof WorldProps | undefined = ELEMENT_TYPE_KEYS[queryRow.element_type];
if (elementKey) {
(worlds[worlds.length - 1][elementKey] as WorldElement[]).push(worldElement);
}
}
} else {
const worldElement: WorldElement = {
id: queryRow.element_id as string,
name: queryRow.element_name ? System.decryptDataWithUserKey(queryRow.element_name, userEncryptionKey) : '',
description: queryRow.element_description ? System.decryptDataWithUserKey(queryRow.element_description, userEncryptionKey) : ''
};
const elementKey: keyof WorldProps | undefined = ELEMENT_TYPE_KEYS[queryRow.element_type as number];
if (elementKey) {
(existingWorld[elementKey] as WorldElement[]).push(worldElement);
}
}
}
return { worlds, enabled };
}
/**
* Updates a world's properties and all its elements.
* @param userId - The unique identifier of the user
* @param world - The WorldProps object containing updated world data
* @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
* @returns True if the update was successful, false otherwise
*/
public static updateWorld(userId: string, world: WorldProps, lang: 'fr' | 'en' = 'fr'): boolean {
const userEncryptionKey: string = getUserEncryptionKey(userId);
const encryptedName: string = world.name ? System.encryptDataWithUserKey(world.name, userEncryptionKey) : '';
const encryptedHistory: string = world.history ? System.encryptDataWithUserKey(world.history, userEncryptionKey) : '';
const encryptedPolitics: string = world.politics ? System.encryptDataWithUserKey(world.politics, userEncryptionKey) : '';
const encryptedEconomy: string = world.economy ? System.encryptDataWithUserKey(world.economy, userEncryptionKey) : '';
const encryptedReligion: string = world.religion ? System.encryptDataWithUserKey(world.religion, userEncryptionKey) : '';
const encryptedLanguages: string = world.languages ? System.encryptDataWithUserKey(world.languages, userEncryptionKey) : '';
let elementsToUpdate: WorldElementValue[] = [];
const elementCategories: { key: keyof WorldProps; elements: WorldElement[] }[] = [
{ key: 'laws', elements: world.laws },
{ key: 'biomes', elements: world.biomes },
{ key: 'issues', elements: world.issues },
{ key: 'customs', elements: world.customs },
{ key: 'kingdoms', elements: world.kingdoms },
{ key: 'climate', elements: world.climate },
{ key: 'resources', elements: world.resources },
{ key: 'wildlife', elements: world.wildlife },
{ key: 'arts', elements: world.arts },
{ key: 'ethnicGroups', elements: world.ethnicGroups },
{ key: 'socialClasses', elements: world.socialClasses },
{ key: 'importantCharacters', elements: world.importantCharacters }
];
elementCategories.forEach(({ key, elements: categoryElements }) => {
elementsToUpdate = elementsToUpdate.concat(categoryElements.map((worldElement: WorldElement) => {
const encryptedElementName: string = System.encryptDataWithUserKey(worldElement.name, userEncryptionKey);
const hashedElementName: string = System.hashElement(worldElement.name);
const encryptedDescription: string = worldElement.description ? System.encryptDataWithUserKey(worldElement.description, userEncryptionKey) : '';
const elementTypeId: number = World.getElementTypes(key);
return {
id: worldElement.id,
name: encryptedElementName,
hashedName: hashedElementName,
description: encryptedDescription,
type: elementTypeId
};
}));
});
WorldRepository.updateWorld(userId, world.id, encryptedName, System.hashElement(world.name), encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, System.timeStampInSeconds(), lang);
return WorldRepository.updateWorldElements(userId, elementsToUpdate, lang);
}
/**
* Adds a new element to an existing world.
* @param userId - The unique identifier of the user
* @param worldId - The unique identifier of the world to add the element to
* @param elementName - The name of the new element
* @param elementType - The type of element (e.g., 'laws', 'biomes', 'customs')
* @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
* @param existingElementId - Optional existing element ID for syncing purposes
* @returns The unique identifier of the newly created element
* @throws Error if an element with the same name already exists in this world
*/
public static addNewElementToWorld(userId: string, worldId: string, elementName: string, elementType: string, lang: 'fr' | 'en' = 'fr', existingElementId?: string): string {
const userEncryptionKey: string = getUserEncryptionKey(userId);
const hashedElementName: string = System.hashElement(elementName);
if (!existingElementId && WorldRepository.checkElementExist(worldId, hashedElementName, lang)) {
throw new Error(lang === "fr" ? `Vous avez déjà un élément avec ce nom ${elementName}.` : `You already have an element named ${elementName}.`);
}
const elementTypeId: number = World.getElementTypes(elementType);
const encryptedElementName: string = System.encryptDataWithUserKey(elementName, userEncryptionKey);
const elementId: string = existingElementId || System.createUniqueId();
return WorldRepository.insertNewElement(userId, elementId, elementTypeId, worldId, encryptedElementName, hashedElementName, lang);
}
/**
* Converts an element type string key to its corresponding numeric identifier.
* @param elementType - The element type key (e.g., 'laws', 'biomes', 'customs')
* @returns The numeric identifier for the element type, or 0 if not found
*/
public static getElementTypes(elementType: string): number {
return ELEMENT_TYPE_MAP[elementType] ?? 0;
}
/**
* Removes an element from a world.
* @param userId - The unique identifier of the user
* @param elementId - The unique identifier of the element to remove
* @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
* @returns True if the deletion was successful, false otherwise
*/
public static removeElementFromWorld(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return WorldRepository.deleteElement(userId, elementId, lang);
}
}