import SpellRepo, { SpellResult } from '../repositories/spell.repo.js'; import SpellTagRepo, { SpellTagResult } from '../repositories/spelltag.repo.js'; import BookRepo, { BookToolsTable } from '../repositories/book.repository.js'; import System from '../System.js'; import { getUserEncryptionKey } from '../keyManager.js'; export interface SpellTagProps { id: string; name: string; color: string | null; } export interface SpellProps { id: string; name: string; description: string; appearance: string; tags: string[]; powerLevel: string | null; components: string | null; limitations: string | null; notes: string | null; seriesSpellId: string | null; } export interface SpellListItem { id: string; name: string; description: string; tags: SpellTagProps[]; seriesSpellId?: string | null; } export interface SpellListResponse { enabled: boolean; spells: SpellListItem[]; tags: SpellTagProps[]; } export interface SyncedSpell { id: string; name: string; lastUpdate: number; } export interface SyncedSpellTag { id: string; name: string; lastUpdate: number; } export default class Spell { /** * Retrieves all spell tags 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') * @returns An array of spell tag props */ static getSpellTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellTagProps[] { const userKey: string = getUserEncryptionKey(userId); const spellTags: SpellTagResult[] = SpellTagRepo.fetchSpellTags(userId, bookId, lang); return spellTags.map((tag: SpellTagResult): SpellTagProps => ({ id: tag.tag_id, name: System.decryptDataWithUserKey(tag.name, userKey), color: tag.color, })); } /** * Adds a new spell tag to a book. * @param userId - The unique identifier of the user * @param bookId - The unique identifier of the book * @param name - The name of the tag * @param color - The optional color hex code * @param existingTagId - Optional existing tag ID for sync * @param lang - The language for error messages ('fr' or 'en') * @returns The created spell tag props */ static addSpellTag(userId: string, bookId: string, name: string, color: string | null, existingTagId?: string, lang: 'fr' | 'en' = 'fr'): SpellTagProps { const userKey: string = getUserEncryptionKey(userId); const tagId: string = existingTagId || System.createUniqueId(); const encryptedName: string = System.encryptDataWithUserKey(name, userKey); const nameHash: string = System.hashElement(name); SpellTagRepo.insertSpellTag(tagId, bookId, userId, encryptedName, nameHash, color, lang); return { id: tagId, name: name, color: color, }; } /** * Updates an existing spell tag. * @param userId - The unique identifier of the user * @param tagId - The unique identifier of the tag * @param name - The new name of the tag * @param color - The new optional color hex code * @param lang - The language for error messages ('fr' or 'en') * @returns True if the update was successful */ static updateSpellTag(userId: string, tagId: string, name: string, color: string | null, lang: 'fr' | 'en' = 'fr'): boolean { const userKey: string = getUserEncryptionKey(userId); const encryptedName: string = System.encryptDataWithUserKey(name, userKey); const nameHash: string = System.hashElement(name); return SpellTagRepo.updateSpellTag(userId, tagId, encryptedName, nameHash, color, lang); } /** * Deletes a spell tag and removes its references from all spells in the book. * @param userId - The unique identifier of the user * @param tagId - The unique identifier of the tag to delete * @param bookId - The unique identifier of the book * @param lang - The language for error messages ('fr' or 'en') * @returns True if the deletion was successful */ static deleteSpellTag(userId: string, tagId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean { const userKey: string = getUserEncryptionKey(userId); const spells: SpellResult[] = SpellRepo.fetchSpells(userId, bookId, lang); for (const spell of spells) { const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey); let tagsArray: string[] = []; try { tagsArray = JSON.parse(decryptedTags) as string[]; } catch { tagsArray = []; } if (tagsArray.includes(tagId)) { const updatedTags: string[] = tagsArray.filter((t: string): boolean => t !== tagId); const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(updatedTags), userKey); SpellRepo.updateSpellTags(userId, spell.spell_id, encryptedTags, lang); } } // Then delete the tag return SpellTagRepo.deleteSpellTag(userId, tagId, lang); } /** * Retrieves the spell list with tags 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') * @returns The spell list response with enabled status, spells, and tags */ static getSpellList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellListResponse { const userKey: string = getUserEncryptionKey(userId); const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang); const enabled: boolean = bookTools ? bookTools.spells_enabled === 1 : false; const spellTags: SpellTagResult[] = SpellTagRepo.fetchSpellTags(userId, bookId, lang); const tags: SpellTagProps[] = spellTags.map((tag: SpellTagResult): SpellTagProps => ({ id: tag.tag_id, name: System.decryptDataWithUserKey(tag.name, userKey), color: tag.color, })); const tagMap: Map = new Map(); for (const tag of tags) { tagMap.set(tag.id, tag); } const spellResults: SpellResult[] = SpellRepo.fetchSpells(userId, bookId, lang); const spells: SpellListItem[] = spellResults.map((spell: SpellResult): SpellListItem => { const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey); const decryptedDescription: string = System.decryptDataWithUserKey(spell.description, userKey); const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey); let tagIds: string[]; try { tagIds = JSON.parse(decryptedTags) as string[]; } catch { tagIds = []; } const resolvedTags: SpellTagProps[] = tagIds .map((tagId: string): SpellTagProps | undefined => tagMap.get(tagId)) .filter((tag: SpellTagProps | undefined): tag is SpellTagProps => tag !== undefined); const truncatedDescription: string = decryptedDescription.length > 150 ? decryptedDescription.substring(0, 150) + '...' : decryptedDescription; return { id: spell.spell_id, name: decryptedName, description: truncatedDescription, tags: resolvedTags, seriesSpellId: spell.series_spell_id || null, }; }); return { enabled, spells, tags, }; } /** * Retrieves the full details of a specific spell. * @param userId - The unique identifier of the user * @param spellId - The unique identifier of the spell * @param lang - The language for error messages ('fr' or 'en') * @returns The spell props with all details */ static getSpellDetail(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SpellProps { const userKey: string = getUserEncryptionKey(userId); const spell: SpellResult | null = SpellRepo.fetchSpellById(userId, spellId, lang); if (!spell) { throw new Error(lang === 'fr' ? 'Sort non trouvé.' : 'Spell not found.'); } const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey); const decryptedDescription: string = System.decryptDataWithUserKey(spell.description, userKey); const decryptedAppearance: string = System.decryptDataWithUserKey(spell.appearance, userKey); const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey); let tagIds: string[]; try { tagIds = JSON.parse(decryptedTags) as string[]; } catch { tagIds = []; } return { id: spell.spell_id, name: decryptedName, description: decryptedDescription, appearance: decryptedAppearance, tags: tagIds, powerLevel: spell.power_level ? System.decryptDataWithUserKey(spell.power_level, userKey) : null, components: spell.components ? System.decryptDataWithUserKey(spell.components, userKey) : null, limitations: spell.limitations ? System.decryptDataWithUserKey(spell.limitations, userKey) : null, notes: spell.notes ? System.decryptDataWithUserKey(spell.notes, userKey) : null, seriesSpellId: spell.series_spell_id || null, }; } /** * Adds a new spell to a book. * @param userId - The unique identifier of the user * @param bookId - The unique identifier of the book * @param name - The name of the spell * @param description - The description of the spell * @param appearance - The appearance of the spell * @param tags - The tag IDs array * @param powerLevel - The optional power level * @param components - The optional components * @param limitations - The optional limitations * @param notes - The optional notes * @param existingSpellId - Optional existing spell ID for sync * @param lang - The language for error messages ('fr' or 'en') * @returns The created spell props */ static addSpell(userId: string, bookId: string, name: string, description: string, appearance: string, tags: string[], powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, existingSpellId?: string, lang: 'fr' | 'en' = 'fr', seriesSpellId: string | null = null): SpellProps { const userKey: string = getUserEncryptionKey(userId); const spellId: string = existingSpellId || System.createUniqueId(); const encryptedName: string = System.encryptDataWithUserKey(name, userKey); const nameHash: string = System.hashElement(name); const encryptedDescription: string = System.encryptDataWithUserKey(description, userKey); const encryptedAppearance: string = System.encryptDataWithUserKey(appearance, userKey); const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(tags), userKey); const encryptedPowerLevel: string | null = powerLevel ? System.encryptDataWithUserKey(powerLevel, userKey) : null; const encryptedComponents: string | null = components ? System.encryptDataWithUserKey(components, userKey) : null; const encryptedLimitations: string | null = limitations ? System.encryptDataWithUserKey(limitations, userKey) : null; const encryptedNotes: string | null = notes ? System.encryptDataWithUserKey(notes, userKey) : null; SpellRepo.insertSpell( spellId, bookId, userId, encryptedName, nameHash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, lang, seriesSpellId, ); return { id: spellId, name, description, appearance, tags, powerLevel, components, limitations, notes, seriesSpellId, }; } /** * Updates an existing spell. * @param userId - The unique identifier of the user * @param spellId - The unique identifier of the spell * @param name - The name of the spell * @param description - The description of the spell * @param appearance - The appearance of the spell * @param tags - The tag IDs array * @param powerLevel - The optional power level * @param components - The optional components * @param limitations - The optional limitations * @param notes - The optional notes * @param lang - The language for error messages ('fr' or 'en') * @returns True if the update was successful */ static updateSpell(userId: string, spellId: string, name: string, description: string, appearance: string, tags: string[], powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr', seriesSpellId: string | null = null): boolean { const userKey: string = getUserEncryptionKey(userId); const encryptedName: string = System.encryptDataWithUserKey(name, userKey); const nameHash: string = System.hashElement(name); const encryptedDescription: string = System.encryptDataWithUserKey(description, userKey); const encryptedAppearance: string = System.encryptDataWithUserKey(appearance, userKey); const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(tags), userKey); const encryptedPowerLevel: string | null = powerLevel ? System.encryptDataWithUserKey(powerLevel, userKey) : null; const encryptedComponents: string | null = components ? System.encryptDataWithUserKey(components, userKey) : null; const encryptedLimitations: string | null = limitations ? System.encryptDataWithUserKey(limitations, userKey) : null; const encryptedNotes: string | null = notes ? System.encryptDataWithUserKey(notes, userKey) : null; return SpellRepo.updateSpell( userId, spellId, encryptedName, nameHash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, lang, seriesSpellId, ); } /** * Deletes a spell. * @param userId - The unique identifier of the user * @param spellId - The unique identifier of the spell * @param lang - The language for error messages ('fr' or 'en') * @returns True if the deletion was successful */ static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean { return SpellRepo.deleteSpell(userId, spellId, lang); } }