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"; import RemovedItem from "./RemovedItem.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[]; seriesWorldId?: string | null; } export interface WorldListResponse { worlds: WorldProps[]; enabled: boolean; } /** * Mapping of element type keys to their corresponding numeric type identifiers. */ const ELEMENT_TYPE_MAP: Record = { 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 = { 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, seriesWorldId: string | null = null): 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, seriesWorldId); } /** * 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: [], seriesWorldId: queryRow.series_world_id || null, }; 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, world.seriesWorldId || null); 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 bookId - The unique identifier of the book * @param elementId - The unique identifier of the element to remove * @param deletedAt - The timestamp of deletion * @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, bookId: string, elementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { const deleted: boolean = WorldRepository.deleteElement(userId, elementId, lang); if (deleted) { RemovedItem.deleteTracker(userId, bookId, 'book_world_elements', elementId, deletedAt, lang); } return deleted; } }