diff --git a/electron/database/models/Book.ts b/electron/database/models/Book.ts index a0d72ed..47c1f59 100644 --- a/electron/database/models/Book.ts +++ b/electron/database/models/Book.ts @@ -39,6 +39,10 @@ import UserRepo from "../repositories/user.repository.js"; export interface SyncedBookTools { lastUpdate: number; + charactersEnabled: boolean; + worldsEnabled: boolean; + locationsEnabled: boolean; + spellsEnabled: boolean; } export interface BookToolsSettings { @@ -55,11 +59,12 @@ export interface BookProps { title: string; subTitle?: string; summary?: string; - serieId?: number; - desiredReleaseDate?: string; - desiredWordCount?: number; + serieId?: number | null; + seriesId?: string | null; + desiredReleaseDate?: string | null; + desiredWordCount?: number | null; wordCount?: number; - coverImage?: string; + coverImage?: string | null; bookMeta?: string; tools?: BookToolsSettings; } @@ -145,6 +150,234 @@ export interface CompleteBookData { chapters: CompleteChapterContent[]; } +// ===== SERIES TABLE INTERFACES (for sync) ===== + +export interface SeriesTable { + series_id: string; + user_id: string; + name: string; + hashed_name: string; + description: string | null; + cover_image: string | null; + last_update: number; +} + +export interface SeriesBooksTable { + series_id: string; + book_id: string; + book_order: number; + last_update: number; +} + +export interface SeriesCharactersTable { + character_id: string; + series_id: string; + user_id: string; + first_name: string; + last_name: string | null; + nickname: string | null; + age: number | null; + gender: string | null; + species: string | null; + nationality: string | null; + status: string | null; + title: string | null; + category: string; + image: string | null; + role: string | null; + biography: string | null; + history: string | null; + speech_pattern: string | null; + catchphrase: string | null; + residence: string | null; + notes: string | null; + color: string | null; + last_update: number; +} + +export interface SeriesCharacterAttributesTable { + attr_id: string; + character_id: string; + user_id: string; + attribute_name: string; + attribute_value: string; + last_update: number; +} + +export interface SeriesWorldsTable { + world_id: string; + series_id: string; + user_id: string; + name: string; + hashed_name: string; + history: string | null; + politics: string | null; + economy: string | null; + religion: string | null; + languages: string | null; + last_update: number; +} + +export interface SeriesWorldElementsTable { + element_id: string; + world_id: string; + user_id: string; + element_type: number; + name: string; + original_name: string; + description: string | null; + last_update: number; +} + +export interface SeriesLocationsTable { + loc_id: string; + series_id: string; + user_id: string; + loc_name: string; + loc_original_name: string; + last_update: number; +} + +export interface SeriesLocationElementsTable { + element_id: string; + location_id: string; + user_id: string; + element_name: string; + original_name: string; + element_description: string | null; + last_update: number; +} + +export interface SeriesLocationSubElementsTable { + sub_element_id: string; + element_id: string; + user_id: string; + sub_elem_name: string; + original_name: string; + sub_elem_description: string | null; + last_update: number; +} + +export interface SeriesSpellsTable { + spell_id: string; + series_id: string; + user_id: string; + name: string; + name_hash: string; + description: string; + appearance: string; + tags: string; + power_level: string | null; + components: string | null; + limitations: string | null; + notes: string | null; + last_update: number; +} + +export interface SeriesSpellTagsTable { + tag_id: string; + series_id: string; + user_id: string; + name: string; + hashed_name: string; + color: string | null; + last_update: number; +} + +// ===== COMPLETE SERIES INTERFACE (for full sync) ===== + +export interface CompleteSeries { + series: SeriesTable[]; + seriesBooks: SeriesBooksTable[]; + seriesCharacters: SeriesCharactersTable[]; + seriesCharacterAttributes: SeriesCharacterAttributesTable[]; + seriesWorlds: SeriesWorldsTable[]; + seriesWorldElements: SeriesWorldElementsTable[]; + seriesLocations: SeriesLocationsTable[]; + seriesLocationElements: SeriesLocationElementsTable[]; + seriesLocationSubElements: SeriesLocationSubElementsTable[]; + seriesSpells: SeriesSpellsTable[]; + seriesSpellTags: SeriesSpellTagsTable[]; +} + +// ===== SYNCED SERIES INTERFACES (lightweight, for comparison) ===== + +export interface SyncedSeriesBook { + bookId: string; + order: number; + lastUpdate: number; +} + +export interface SyncedSeriesCharacterAttribute { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedSeriesCharacter { + id: string; + name: string; + lastUpdate: number; + attributes: SyncedSeriesCharacterAttribute[]; +} + +export interface SyncedSeriesWorldElement { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedSeriesWorld { + id: string; + name: string; + lastUpdate: number; + elements: SyncedSeriesWorldElement[]; +} + +export interface SyncedSeriesLocationSubElement { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedSeriesLocationElement { + id: string; + name: string; + lastUpdate: number; + subElements: SyncedSeriesLocationSubElement[]; +} + +export interface SyncedSeriesLocation { + id: string; + name: string; + lastUpdate: number; + elements: SyncedSeriesLocationElement[]; +} + +export interface SyncedSeriesSpell { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedSeriesSpellTag { + id: string; + name: string; + lastUpdate: number; +} + +export interface SyncedSeries { + id: string; + name: string; + lastUpdate: number; + books: SyncedSeriesBook[]; + characters: SyncedSeriesCharacter[]; + worlds: SyncedSeriesWorld[]; + locations: SyncedSeriesLocation[]; + spells: SyncedSeriesSpell[]; + spellTags: SyncedSeriesSpellTag[]; +} + export default class Book { private readonly id: string; private type: string; diff --git a/electron/database/models/Character.ts b/electron/database/models/Character.ts index a26101a..1cbda1a 100644 --- a/electron/database/models/Character.ts +++ b/electron/database/models/Character.ts @@ -14,11 +14,11 @@ export interface CharacterPropsPost { name: string; lastName: string; nickname: string; - age: string; + age: number | null; gender: string; species: string; nationality: string; - status: 'alive' | 'dead' | 'unknown'; + status: string; category: CharacterCategory; title: string; image: string; @@ -48,6 +48,7 @@ export interface CharacterPropsPost { residence?: string; notes?: string; color?: string; + seriesCharacterId?: string | null; } @@ -56,7 +57,7 @@ export interface CharacterProps { name: string; lastName: string; nickname: string; - age: string; + age: number | null; gender: string; species: string; nationality: string; @@ -72,6 +73,7 @@ export interface CharacterProps { residence: string; notes: string; color: string; + seriesCharacterId: string | null; } export interface CharacterListResponse { @@ -83,24 +85,24 @@ export interface CompleteCharacterProps { id?: string; name: string; lastName: string; - nickname?: string; - age?: string; - gender?: string; - species?: string; - nationality?: string; - status?: string; + nickname: string; + age: number | null; + gender: string; + species: string; + nationality: string; + status: string; title: string; category: string; image?: string; role: string; biography: string; history: string; - speechPattern?: string; - catchphrase?: string; - residence?: string; - notes?: string; - color?: string; - [key: string]: Attribute[] | string | undefined; + speechPattern: string; + catchphrase: string; + residence: string; + notes: string; + color: string; + [key: string]: Attribute[] | string | number | null | undefined; } export interface Attribute { @@ -152,7 +154,7 @@ export default class Character { name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '', lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '', nickname: encryptedCharacter.nickname ? System.decryptDataWithUserKey(encryptedCharacter.nickname, userEncryptionKey) : '', - age: encryptedCharacter.age ? System.decryptDataWithUserKey(encryptedCharacter.age, userEncryptionKey) : '', + age: encryptedCharacter.age ? parseInt(System.decryptDataWithUserKey(encryptedCharacter.age, userEncryptionKey), 10) : null, gender: encryptedCharacter.gender ? System.decryptDataWithUserKey(encryptedCharacter.gender, userEncryptionKey) : '', species: encryptedCharacter.species ? System.decryptDataWithUserKey(encryptedCharacter.species, userEncryptionKey) : '', nationality: encryptedCharacter.nationality ? System.decryptDataWithUserKey(encryptedCharacter.nationality, userEncryptionKey) : '', @@ -168,6 +170,7 @@ export default class Character { residence: encryptedCharacter.residence ? System.decryptDataWithUserKey(encryptedCharacter.residence, userEncryptionKey) : '', notes: encryptedCharacter.notes ? System.decryptDataWithUserKey(encryptedCharacter.notes, userEncryptionKey) : '', color: encryptedCharacter.color ? System.decryptDataWithUserKey(encryptedCharacter.color, userEncryptionKey) : '', + seriesCharacterId: encryptedCharacter.series_character_id || null, }) } return { characters: decryptedCharacterList, enabled }; @@ -191,7 +194,7 @@ export default class Character { firstName: System.encryptDataWithUserKey(character.name, userEncryptionKey), lastName: System.encryptDataWithUserKey(character.lastName, userEncryptionKey), nickname: System.encryptDataWithUserKey(character.nickname || '', userEncryptionKey), - age: System.encryptDataWithUserKey(character.age || '', userEncryptionKey), + age: character.age !== null ? System.encryptDataWithUserKey(String(character.age), userEncryptionKey) : '', gender: System.encryptDataWithUserKey(character.gender || '', userEncryptionKey), species: System.encryptDataWithUserKey(character.species || '', userEncryptionKey), nationality: System.encryptDataWithUserKey(character.nationality || '', userEncryptionKey), @@ -209,7 +212,7 @@ export default class Character { color: System.encryptDataWithUserKey(character.color || '', userEncryptionKey), }; - CharacterRepo.addNewCharacter(userId, characterId, characterData, bookId, lang); + CharacterRepo.addNewCharacter(userId, characterId, characterData, bookId, lang, character.seriesCharacterId || null); const characterPropertyKeys: string[] = Object.keys(character); for (const propertyKey of characterPropertyKeys) { if (Array.isArray(character[propertyKey as keyof CharacterPropsPost])) { @@ -244,7 +247,7 @@ export default class Character { firstName: System.encryptDataWithUserKey(character.name, userEncryptionKey), lastName: System.encryptDataWithUserKey(character.lastName, userEncryptionKey), nickname: System.encryptDataWithUserKey(character.nickname || '', userEncryptionKey), - age: System.encryptDataWithUserKey(character.age || '', userEncryptionKey), + age: character.age !== null ? System.encryptDataWithUserKey(String(character.age), userEncryptionKey) : '', gender: System.encryptDataWithUserKey(character.gender || '', userEncryptionKey), species: System.encryptDataWithUserKey(character.species || '', userEncryptionKey), nationality: System.encryptDataWithUserKey(character.nationality || '', userEncryptionKey), @@ -262,7 +265,7 @@ export default class Character { color: System.encryptDataWithUserKey(character.color || '', userEncryptionKey), }; - return CharacterRepo.updateCharacter(userId, character.id, characterData, System.timeStampInSeconds(), lang); + return CharacterRepo.updateCharacter(userId, character.id, characterData, System.timeStampInSeconds(), lang, character.seriesCharacterId || null); } /** @@ -370,7 +373,7 @@ export default class Character { name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '', lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '', nickname: encryptedCharacter.nickname ? System.decryptDataWithUserKey(encryptedCharacter.nickname as string, userEncryptionKey) : '', - age: encryptedCharacter.age ? System.decryptDataWithUserKey(encryptedCharacter.age as string, userEncryptionKey) : '', + age: encryptedCharacter.age ? parseInt(System.decryptDataWithUserKey(encryptedCharacter.age as string, userEncryptionKey), 10) : null, gender: encryptedCharacter.gender ? System.decryptDataWithUserKey(encryptedCharacter.gender as string, userEncryptionKey) : '', species: encryptedCharacter.species ? System.decryptDataWithUserKey(encryptedCharacter.species as string, userEncryptionKey) : '', nationality: encryptedCharacter.nationality ? System.decryptDataWithUserKey(encryptedCharacter.nationality as string, userEncryptionKey) : '', @@ -442,11 +445,22 @@ export default class Character { uniqueCharactersMap.set(characterIdentifier, { name: character.name, lastName: character.lastName, - category: character.category, + nickname: character.nickname, + age: character.age, + gender: character.gender, + species: character.species, + nationality: character.nationality, + status: character.status, title: character.title, + category: character.category, role: character.role, biography: character.biography, - history: character.history + history: character.history, + speechPattern: character.speechPattern, + catchphrase: character.catchphrase, + residence: character.residence, + notes: character.notes, + color: character.color }); } @@ -472,7 +486,7 @@ export default class Character { }); Object.keys(character).forEach((propertyKey: string): void => { - const propertyValue: string | Attribute[] | undefined = character[propertyKey]; + const propertyValue = character[propertyKey]; if (Array.isArray(propertyValue) && propertyValue.length > 0) { const capitalizedPropertyKey: string = propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1); const formattedAttributeValues: string = propertyValue.map((attributeItem: Attribute) => attributeItem.name).join(', '); diff --git a/electron/database/models/Location.ts b/electron/database/models/Location.ts index e0634e5..6d6c4aa 100644 --- a/electron/database/models/Location.ts +++ b/electron/database/models/Location.ts @@ -24,6 +24,7 @@ export interface LocationProps { id: string; name: string; elements: Element[]; + seriesLocationId?: string | null; } export interface LocationListResponse { @@ -79,7 +80,8 @@ export default class Location { location = { id: record.loc_id, name: decryptedName, - elements: [] + elements: [], + seriesLocationId: record.series_location_id || null, }; locationArray.push(location); } @@ -127,12 +129,12 @@ export default class Location { * @param existingLocationId - Optional existing location ID to use instead of generating a new one. * @returns The ID of the created location. */ - static addLocationSection(userId: string, locationName: string, bookId: string, lang: 'fr' | 'en' = 'fr', existingLocationId?: string): string { + static addLocationSection(userId: string, locationName: string, bookId: string, lang: 'fr' | 'en' = 'fr', existingLocationId?: string, seriesLocationId: string | null = null): string { const userKey: string = getUserEncryptionKey(userId); const hashedName: string = System.hashElement(locationName); const encryptedName: string = System.encryptDataWithUserKey(locationName, userKey); const locationId: string = existingLocationId || System.createUniqueId(); - return LocationRepo.insertLocation(userId, locationId, bookId, encryptedName, hashedName, lang); + return LocationRepo.insertLocation(userId, locationId, bookId, encryptedName, hashedName, lang, seriesLocationId); } /** @@ -202,6 +204,28 @@ export default class Location { } } + /** + * Updates a location section with optional name change and series link. + * @param userId - The unique identifier of the user + * @param sectionId - The unique identifier of the section + * @param sectionName - The new name (optional) + * @param seriesLocationId - The series location ID to link (optional, null to unlink) + * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. + * @returns True if the update was successful + */ + static updateSectionWithSeriesLink(userId: string, sectionId: string, sectionName?: string, seriesLocationId?: string | null, lang: 'fr' | 'en' = 'fr'): boolean { + let encryptedName: string | null = null; + let originalNameHash: string | null = null; + + if (sectionName) { + const userKey: string = getUserEncryptionKey(userId); + encryptedName = System.encryptDataWithUserKey(sectionName, userKey); + originalNameHash = System.hashElement(sectionName); + } + + return LocationRepo.updateSectionWithSeriesLink(userId, sectionId, encryptedName, originalNameHash, seriesLocationId ?? null, lang); + } + /** * Deletes a location section and all its associated elements and sub-elements. * @param userId - The user's unique identifier. diff --git a/electron/database/models/Series.ts b/electron/database/models/Series.ts new file mode 100644 index 0000000..113ed5f --- /dev/null +++ b/electron/database/models/Series.ts @@ -0,0 +1,248 @@ +import { getUserEncryptionKey } from "../keyManager.js"; +import System from "../System.js"; +import SeriesRepo, { SeriesBookResult, SeriesListItem, SeriesResult } from "../repositories/series.repo.js"; + +export interface SeriesProps { + id: string; + name: string; + description: string; + coverImage: string | null; +} + +export interface SeriesDetailProps { + id: string; + name: string; + description: string; + coverImage: string | null; + books: SeriesBookProps[]; +} + +export interface SeriesBookProps { + bookId: string; + title: string; + order: number; + coverImage: string | null; +} + +export interface SeriesListItemProps { + id: string; + name: string; + description: string; + coverImage: string | null; + bookCount: number; + bookIds: string[]; +} + +export interface BooksOrderPost { + bookId: string; + order: number; +} + +export default class Series { + /** + * Gets the list of all series for a user. + * @param userId - The unique identifier of the user + * @param lang - The language for error messages ('fr' or 'en') + * @returns The list of series with decrypted names and descriptions + */ + public static getSeriesList(userId: string, lang: 'fr' | 'en' = 'fr'): SeriesListItemProps[] { + const userKey: string = getUserEncryptionKey(userId); + const seriesResults: SeriesListItem[] = SeriesRepo.fetchUserSeries(userId, lang); + + return seriesResults.map((seriesItem: SeriesListItem): SeriesListItemProps => ({ + id: seriesItem.series_id, + name: System.decryptDataWithUserKey(seriesItem.name, userKey), + description: seriesItem.description ? System.decryptDataWithUserKey(seriesItem.description, userKey) : '', + coverImage: seriesItem.cover_image, + bookCount: seriesItem.book_count, + bookIds: seriesItem.book_ids ? seriesItem.book_ids.split(',') : [] + })); + } + + /** + * Gets the detail of a series including its books. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns The series detail with decrypted data + */ + public static getSeriesDetail(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesDetailProps { + const userKey: string = getUserEncryptionKey(userId); + + const seriesResult: SeriesResult | null = SeriesRepo.fetchSeriesById(userId, seriesId, lang); + if (!seriesResult) { + throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.'); + } + + const booksResult: SeriesBookResult[] = SeriesRepo.fetchSeriesBooks(userId, seriesId, lang); + + const books: SeriesBookProps[] = booksResult.map((book: SeriesBookResult) => ({ + bookId: book.book_id, + title: System.decryptDataWithUserKey(book.title, userKey), + order: book.book_order, + coverImage: book.cover_image + })); + + return { + id: seriesResult.series_id, + name: System.decryptDataWithUserKey(seriesResult.name, userKey), + description: seriesResult.description ? System.decryptDataWithUserKey(seriesResult.description, userKey) : '', + coverImage: seriesResult.cover_image, + books + }; + } + + /** + * Creates a new series. + * @param userId - The unique identifier of the user + * @param name - The name of the series + * @param description - The description of the series + * @param lang - The language for error messages ('fr' or 'en') + * @param bookIds - Optional array of book IDs to add to the series + * @returns The created series ID + */ + public static createSeries(userId: string, name: string, description: string, lang: 'fr' | 'en' = 'fr', bookIds?: string[]): string { + const userKey: string = getUserEncryptionKey(userId); + const seriesId: string = System.createUniqueId(); + + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + const hashedName: string = System.hashElement(name); + const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null; + + SeriesRepo.insertSeries(seriesId, userId, encryptedName, hashedName, encryptedDescription, lang); + + if (bookIds && bookIds.length > 0) { + for (let i: number = 0; i < bookIds.length; i++) { + SeriesRepo.addBookToSeries(seriesId, bookIds[i], i + 1, lang); + } + } + + return seriesId; + } + + /** + * Updates an existing series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param name - The name of the series + * @param description - The description of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the update was successful + */ + public static updateSeries(userId: string, seriesId: string, name: string, description: string, lang: 'fr' | 'en' = 'fr'): boolean { + const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang); + if (!exists) { + throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.'); + } + + const userKey: string = getUserEncryptionKey(userId); + + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + const hashedName: string = System.hashElement(name); + const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null; + + return SeriesRepo.updateSeries(userId, seriesId, encryptedName, hashedName, encryptedDescription, lang); + } + + /** + * Deletes a series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the deletion was successful + */ + public static deleteSeries(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): boolean { + const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang); + if (!exists) { + throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.'); + } + + return SeriesRepo.deleteSeries(userId, seriesId, lang); + } + + /** + * Adds a book to a series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param bookId - The unique identifier of the book + * @param order - The order of the book in the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the addition was successful + */ + public static addBookToSeries(userId: string, seriesId: string, bookId: string, order: number, lang: 'fr' | 'en' = 'fr'): boolean { + const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang); + if (!exists) { + throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.'); + } + + return SeriesRepo.addBookToSeries(seriesId, bookId, order, lang); + } + + /** + * Removes a book from a series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param bookId - The unique identifier of the book + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the removal was successful + */ + public static removeBookFromSeries(userId: string, seriesId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean { + const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang); + if (!exists) { + throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.'); + } + + return SeriesRepo.removeBookFromSeries(seriesId, bookId, lang); + } + + /** + * Updates the order of books in a series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param booksOrder - An array of {bookId, order} objects + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the update was successful + */ + public static updateBooksOrder(userId: string, seriesId: string, booksOrder: BooksOrderPost[], lang: 'fr' | 'en' = 'fr'): boolean { + const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang); + if (!exists) { + throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.'); + } + + return SeriesRepo.updateBooksOrder(seriesId, booksOrder, lang); + } + + /** + * Gets the series ID for a book if it belongs to one. + * @param bookId - The unique identifier of the book + * @returns The series ID or null + */ + public static getSeriesIdForBook(bookId: string): string | null { + return SeriesRepo.getSeriesIdForBook(bookId); + } + + /** + * Gets only the books of a series (without series details). + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns The list of books in the series + */ + public static getSeriesBooks(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesBookProps[] { + const userKey: string = getUserEncryptionKey(userId); + + const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang); + if (!exists) { + throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.'); + } + + const booksResult: SeriesBookResult[] = SeriesRepo.fetchSeriesBooks(userId, seriesId, lang); + + return booksResult.map((book: SeriesBookResult): SeriesBookProps => ({ + bookId: book.book_id, + title: System.decryptDataWithUserKey(book.title, userKey), + order: book.book_order, + coverImage: book.cover_image + })); + } +} diff --git a/electron/database/models/SeriesCharacter.ts b/electron/database/models/SeriesCharacter.ts new file mode 100644 index 0000000..aaf043b --- /dev/null +++ b/electron/database/models/SeriesCharacter.ts @@ -0,0 +1,276 @@ +import { getUserEncryptionKey } from "../keyManager.js"; +import System from "../System.js"; +import SeriesCharacterRepo, { SeriesCharacterAttributeResult, SeriesCharacterResult } from "../repositories/series-character.repo.js"; + +export type CharacterCategory = 'Main' | 'Secondary' | 'Recurring'; + +export interface SeriesCharacterPropsPost { + id: string | null; + name: string; + lastName: string; + nickname: string; + age: number | null; + gender: string; + species: string; + nationality: string; + status: string; + category: CharacterCategory; + title: string; + image: string; + physical: { name: string }[]; + psychological: { name: string }[]; + relations: { name: string }[]; + skills: { name: string }[]; + weaknesses: { name: string }[]; + strengths: { name: string }[]; + goals: { name: string }[]; + motivations: { name: string }[]; + arc: { name: string }[]; + secrets: { name: string }[]; + fears: { name: string }[]; + flaws: { name: string }[]; + beliefs: { name: string }[]; + conflicts: { name: string }[]; + quotes: { name: string }[]; + distinguishingMarks: { name: string }[]; + items: { name: string }[]; + affiliations: { name: string }[]; + role: string; + biography?: string; + history?: string; + speechPattern?: string; + catchphrase?: string; + residence?: string; + notes?: string; + color?: string; +} + +export interface SeriesCharacterListProps { + id: string; + name: string; + lastName: string; + nickname: string; + age: number | null; + gender: string; + species: string; + nationality: string; + status: string; + title: string; + category: string; + image: string; + role: string; + biography: string; + history: string; + speechPattern: string; + catchphrase: string; + residence: string; + notes: string; + color: string; +} + +export interface SeriesAttribute { + id: string; + name: string; +} + +export interface CharacterAttributesResponse { + attributes: SeriesAttribute[]; +} + +export default class SeriesCharacter { + /** + * Retrieves a list of characters for a specific series owned by a user. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns Characters list + */ + public static getCharacterList(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterListProps[] { + const characters: SeriesCharacterResult[] = SeriesCharacterRepo.fetchCharacters(userId, seriesId, lang); + if (!characters || characters.length === 0) { + return []; + } + + const userKey: string = getUserEncryptionKey(userId); + + return characters.map((character: SeriesCharacterResult): SeriesCharacterListProps => ({ + id: character.character_id, + name: character.first_name ? System.decryptDataWithUserKey(character.first_name, userKey) : '', + lastName: character.last_name ? System.decryptDataWithUserKey(character.last_name, userKey) : '', + nickname: character.nickname ? System.decryptDataWithUserKey(character.nickname, userKey) : '', + age: character.age ? parseInt(System.decryptDataWithUserKey(character.age, userKey), 10) : null, + gender: character.gender ? System.decryptDataWithUserKey(character.gender, userKey) : '', + species: character.species ? System.decryptDataWithUserKey(character.species, userKey) : '', + nationality: character.nationality ? System.decryptDataWithUserKey(character.nationality, userKey) : '', + status: character.status ? System.decryptDataWithUserKey(character.status, userKey) : 'alive', + title: character.title ? System.decryptDataWithUserKey(character.title, userKey) : '', + category: character.category ? System.decryptDataWithUserKey(character.category, userKey) : '', + image: character.image ? System.decryptDataWithUserKey(character.image, userKey) : '', + role: character.role ? System.decryptDataWithUserKey(character.role, userKey) : '', + biography: character.biography ? System.decryptDataWithUserKey(character.biography, userKey) : '', + history: character.history ? System.decryptDataWithUserKey(character.history, userKey) : '', + speechPattern: character.speech_pattern ? System.decryptDataWithUserKey(character.speech_pattern, userKey) : '', + catchphrase: character.catchphrase ? System.decryptDataWithUserKey(character.catchphrase, userKey) : '', + residence: character.residence ? System.decryptDataWithUserKey(character.residence, userKey) : '', + notes: character.notes ? System.decryptDataWithUserKey(character.notes, userKey) : '', + color: character.color ? System.decryptDataWithUserKey(character.color, userKey) : '', + })); + } + + /** + * Adds a new character to a series with all its attributes. + * @param userId - The unique identifier of the user + * @param character - The character data to create + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns The newly created character's ID + */ + public static addNewCharacter(userId: string, character: SeriesCharacterPropsPost, seriesId: string, lang: 'fr' | 'en' = 'fr'): string { + const userKey: string = getUserEncryptionKey(userId); + const characterId: string = System.createUniqueId(); + const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey); + const encryptedLastName: string | null = character.lastName ? System.encryptDataWithUserKey(character.lastName, userKey) : null; + const encryptedNickname: string | null = character.nickname ? System.encryptDataWithUserKey(character.nickname, userKey) : null; + const encryptedAge: string | null = character.age !== null ? System.encryptDataWithUserKey(String(character.age), userKey) : null; + const encryptedGender: string | null = character.gender ? System.encryptDataWithUserKey(character.gender, userKey) : null; + const encryptedSpecies: string | null = character.species ? System.encryptDataWithUserKey(character.species, userKey) : null; + const encryptedNationality: string | null = character.nationality ? System.encryptDataWithUserKey(character.nationality, userKey) : null; + const encryptedStatus: string | null = character.status ? System.encryptDataWithUserKey(character.status, userKey) : null; + const encryptedTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userKey) : null; + const encryptedCategory: string | null = character.category ? System.encryptDataWithUserKey(character.category, userKey) : null; + const encryptedImage: string | null = character.image ? System.encryptDataWithUserKey(character.image, userKey) : null; + const encryptedRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userKey) : null; + const encryptedBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userKey) : null; + const encryptedHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userKey) : null; + const encryptedSpeechPattern: string | null = character.speechPattern ? System.encryptDataWithUserKey(character.speechPattern, userKey) : null; + const encryptedCatchphrase: string | null = character.catchphrase ? System.encryptDataWithUserKey(character.catchphrase, userKey) : null; + const encryptedResidence: string | null = character.residence ? System.encryptDataWithUserKey(character.residence, userKey) : null; + const encryptedNotes: string | null = character.notes ? System.encryptDataWithUserKey(character.notes, userKey) : null; + const encryptedColor: string | null = character.color ? System.encryptDataWithUserKey(character.color, userKey) : null; + + SeriesCharacterRepo.addNewCharacter(userId, characterId, encryptedName, encryptedLastName, encryptedNickname, encryptedAge, encryptedGender, encryptedSpecies, encryptedNationality, encryptedStatus, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, encryptedSpeechPattern, encryptedCatchphrase, encryptedResidence, encryptedNotes, encryptedColor, seriesId, lang); + + const attributeKeys: string[] = Object.keys(character); + for (const attributeKey of attributeKeys) { + const attributeValue = character[attributeKey as keyof SeriesCharacterPropsPost]; + if (Array.isArray(attributeValue)) { + const attributeArray: { name: string }[] = attributeValue; + if (attributeArray.length > 0) { + for (const attributeItem of attributeArray) { + const attributeType: string = attributeKey; + const attributeName: string = attributeItem.name; + this.addNewAttribute(characterId, userId, attributeType, attributeName, lang); + } + } + } + } + + return characterId; + } + + /** + * Updates an existing character's information and attributes. + * @param userId - The unique identifier of the user + * @param character - The updated character data + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the update was successful + */ + public static updateCharacter(userId: string, character: SeriesCharacterPropsPost, lang: 'fr' | 'en' = 'fr'): boolean { + if (!character.id) { + throw new Error(lang === 'fr' ? 'ID du personnage requis.' : 'Character ID required.'); + } + + const exists: boolean = SeriesCharacterRepo.isCharacterExist(userId, character.id, lang); + if (!exists) { + throw new Error(lang === 'fr' ? 'Personnage non trouvé.' : 'Character not found.'); + } + + const userKey: string = getUserEncryptionKey(userId); + const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey); + const encryptedLastName: string | null = character.lastName ? System.encryptDataWithUserKey(character.lastName, userKey) : null; + const encryptedNickname: string | null = character.nickname ? System.encryptDataWithUserKey(character.nickname, userKey) : null; + const encryptedAge: string | null = character.age !== null ? System.encryptDataWithUserKey(String(character.age), userKey) : null; + const encryptedGender: string | null = character.gender ? System.encryptDataWithUserKey(character.gender, userKey) : null; + const encryptedSpecies: string | null = character.species ? System.encryptDataWithUserKey(character.species, userKey) : null; + const encryptedNationality: string | null = character.nationality ? System.encryptDataWithUserKey(character.nationality, userKey) : null; + const encryptedStatus: string | null = character.status ? System.encryptDataWithUserKey(character.status, userKey) : null; + const encryptedTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userKey) : null; + const encryptedCategory: string | null = character.category ? System.encryptDataWithUserKey(character.category, userKey) : null; + const encryptedImage: string | null = character.image ? System.encryptDataWithUserKey(character.image, userKey) : null; + const encryptedRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userKey) : null; + const encryptedBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userKey) : null; + const encryptedHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userKey) : null; + const encryptedSpeechPattern: string | null = character.speechPattern ? System.encryptDataWithUserKey(character.speechPattern, userKey) : null; + const encryptedCatchphrase: string | null = character.catchphrase ? System.encryptDataWithUserKey(character.catchphrase, userKey) : null; + const encryptedResidence: string | null = character.residence ? System.encryptDataWithUserKey(character.residence, userKey) : null; + const encryptedNotes: string | null = character.notes ? System.encryptDataWithUserKey(character.notes, userKey) : null; + const encryptedColor: string | null = character.color ? System.encryptDataWithUserKey(character.color, userKey) : null; + + return SeriesCharacterRepo.updateCharacter(userId, character.id, encryptedName, encryptedLastName, encryptedNickname, encryptedAge, encryptedGender, encryptedSpecies, encryptedNationality, encryptedStatus, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, encryptedSpeechPattern, encryptedCatchphrase, encryptedResidence, encryptedNotes, encryptedColor, lang); + } + + /** + * Deletes a character from a series. + * @param userId - The unique identifier of the user + * @param characterId - The unique identifier of the character + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the deletion was successful + */ + public static deleteCharacter(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean { + const exists: boolean = SeriesCharacterRepo.isCharacterExist(userId, characterId, lang); + if (!exists) { + throw new Error(lang === 'fr' ? 'Personnage non trouvé.' : 'Character not found.'); + } + return SeriesCharacterRepo.deleteCharacter(userId, characterId, lang); + } + + /** + * Adds a new attribute to a character. + * @param characterId - The unique identifier of the character + * @param userId - The unique identifier of the user + * @param type - The attribute type + * @param name - The attribute value + * @param lang - The language for error messages ('fr' or 'en') + * @returns The attribute ID + */ + public static addNewAttribute(characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr'): string { + const userKey: string = getUserEncryptionKey(userId); + const attributeId: string = System.createUniqueId(); + const encryptedType: string = System.encryptDataWithUserKey(type, userKey); + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + + SeriesCharacterRepo.insertAttribute(attributeId, characterId, userId, encryptedType, encryptedName, lang); + + return attributeId; + } + + /** + * Deletes an attribute from a character. + * @param userId - The unique identifier of the user + * @param attributeId - The unique identifier of the attribute + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the deletion was successful + */ + public static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr'): boolean { + return SeriesCharacterRepo.deleteAttribute(userId, attributeId, lang); + } + + /** + * Gets all attributes for a character. + * @param userId - The unique identifier of the user + * @param characterId - The unique identifier of the character + * @param lang - The language for error messages ('fr' or 'en') + * @returns The character's attributes + */ + public static getCharacterAttributes(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): CharacterAttributesResponse { + const userKey: string = getUserEncryptionKey(userId); + const attributesResult: SeriesCharacterAttributeResult[] = SeriesCharacterRepo.fetchAttributes(characterId, userId, lang); + + const attributes: SeriesAttribute[] = attributesResult.map((attr) => ({ + id: attr.attr_id, + name: System.decryptDataWithUserKey(attr.attribute_value, userKey) + })); + + return { attributes }; + } +} diff --git a/electron/database/models/SeriesLocation.ts b/electron/database/models/SeriesLocation.ts new file mode 100644 index 0000000..5ca35c8 --- /dev/null +++ b/electron/database/models/SeriesLocation.ts @@ -0,0 +1,154 @@ +import { getUserEncryptionKey } from "../keyManager.js"; +import System from "../System.js"; +import SeriesLocationRepo, { SeriesLocationResult, SeriesLocationElementResult, SeriesLocationSubElementResult } from "../repositories/series-location.repo.js"; + +export interface SeriesLocationSubElementProps { + id: string; + name: string; + description: string; +} + +export interface SeriesLocationElementProps { + id: string; + name: string; + description: string; + subElements: SeriesLocationSubElementProps[]; +} + +export interface SeriesLocationListProps { + id: string; + name: string; + elements: SeriesLocationElementProps[]; +} + +export default class SeriesLocation { + /** + * Retrieves all locations for a series with their elements and sub-elements. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns The list of locations + */ + public static getLocationList(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationListProps[] { + const userKey: string = getUserEncryptionKey(userId); + const locationsResult: SeriesLocationResult[] = SeriesLocationRepo.fetchLocations(userId, seriesId, lang); + + return locationsResult.map((loc): SeriesLocationListProps => { + const elementsResult: SeriesLocationElementResult[] = SeriesLocationRepo.fetchElements(userId, loc.loc_id, lang); + + const elements: SeriesLocationElementProps[] = elementsResult.map((elem): SeriesLocationElementProps => { + const subElementsResult: SeriesLocationSubElementResult[] = SeriesLocationRepo.fetchSubElements(userId, elem.element_id, lang); + + const subElements: SeriesLocationSubElementProps[] = subElementsResult.map((sub): SeriesLocationSubElementProps => ({ + id: sub.sub_element_id, + name: sub.sub_elem_name ? System.decryptDataWithUserKey(sub.sub_elem_name, userKey) : '', + description: sub.sub_elem_description ? System.decryptDataWithUserKey(sub.sub_elem_description, userKey) : '' + })); + + return { + id: elem.element_id, + name: elem.element_name ? System.decryptDataWithUserKey(elem.element_name, userKey) : '', + description: elem.element_description ? System.decryptDataWithUserKey(elem.element_description, userKey) : '', + subElements + }; + }); + + return { + id: loc.loc_id, + name: loc.loc_name ? System.decryptDataWithUserKey(loc.loc_name, userKey) : '', + elements + }; + }); + } + + /** + * Adds a new location section to a series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param name - The name of the location + * @param lang - The language for error messages ('fr' or 'en') + * @returns The new location ID + */ + public static addLocationSection(userId: string, seriesId: string, name: string, lang: 'fr' | 'en' = 'fr'): string { + const userKey: string = getUserEncryptionKey(userId); + const locationId: string = System.createUniqueId(); + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + const originalName: string = System.hashElement(name); + + SeriesLocationRepo.insertLocation(locationId, seriesId, userId, encryptedName, originalName, lang); + return locationId; + } + + /** + * Adds a new element to a location. + * @param userId - The unique identifier of the user + * @param locationId - The unique identifier of the location + * @param name - The name of the element + * @param lang - The language for error messages ('fr' or 'en') + * @param description - The description of the element (optional) + * @returns The new element ID + */ + public static addElement(userId: string, locationId: string, name: string, lang: 'fr' | 'en' = 'fr', description?: string): string { + const userKey: string = getUserEncryptionKey(userId); + const elementId: string = System.createUniqueId(); + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + const originalName: string = System.hashElement(name); + const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null; + + SeriesLocationRepo.insertElement(elementId, locationId, userId, encryptedName, originalName, encryptedDescription, lang); + return elementId; + } + + /** + * Adds a new sub-element to an element. + * @param userId - The unique identifier of the user + * @param elementId - The unique identifier of the element + * @param name - The name of the sub-element + * @param lang - The language for error messages ('fr' or 'en') + * @param description - The description of the sub-element (optional) + * @returns The new sub-element ID + */ + public static addSubElement(userId: string, elementId: string, name: string, lang: 'fr' | 'en' = 'fr', description?: string): string { + const userKey: string = getUserEncryptionKey(userId); + const subElementId: string = System.createUniqueId(); + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + const originalName: string = System.hashElement(name); + const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null; + + SeriesLocationRepo.insertSubElement(subElementId, elementId, userId, encryptedName, originalName, encryptedDescription, lang); + return subElementId; + } + + /** + * Deletes a location section. + * @param userId - The unique identifier of the user + * @param locationId - The unique identifier of the location + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if successful + */ + public static deleteLocation(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): boolean { + return SeriesLocationRepo.deleteLocation(userId, locationId, lang); + } + + /** + * Deletes an element. + * @param userId - The unique identifier of the user + * @param elementId - The unique identifier of the element + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if successful + */ + public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { + return SeriesLocationRepo.deleteElement(userId, elementId, lang); + } + + /** + * Deletes a sub-element. + * @param userId - The unique identifier of the user + * @param subElementId - The unique identifier of the sub-element + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if successful + */ + public static deleteSubElement(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): boolean { + return SeriesLocationRepo.deleteSubElement(userId, subElementId, lang); + } +} diff --git a/electron/database/models/SeriesSpell.ts b/electron/database/models/SeriesSpell.ts new file mode 100644 index 0000000..c386034 --- /dev/null +++ b/electron/database/models/SeriesSpell.ts @@ -0,0 +1,211 @@ +import { getUserEncryptionKey } from "../keyManager.js"; +import System from "../System.js"; +import SeriesSpellRepo, { SeriesSpellResult, SeriesSpellTagResult } from "../repositories/series-spell.repo.js"; + +export interface SeriesSpellTagProps { + id: string; + name: string; + color: string | null; +} + +export interface SeriesSpellListProps { + id: string; + name: string; + description: string; + tags: string[]; +} + +export interface SeriesSpellListResponse { + spells: SeriesSpellListProps[]; + tags: SeriesSpellTagProps[]; +} + +export interface SeriesSpellDetailProps { + id: string; + name: string; + description: string; + appearance: string; + tags: string[]; + powerLevel: string | null; + components: string | null; + limitations: string | null; + notes: string | null; +} + +export default class SeriesSpell { + /** + * Retrieves all spells and tags for a series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns The list of spells and tags + */ + public static getSpellList(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellListResponse { + const userKey: string = getUserEncryptionKey(userId); + const spellsResult: SeriesSpellResult[] = SeriesSpellRepo.fetchSpells(userId, seriesId, lang); + const tagsResult: SeriesSpellTagResult[] = SeriesSpellRepo.fetchTags(userId, seriesId, lang); + + const spells: SeriesSpellListProps[] = spellsResult.map((spell): SeriesSpellListProps => ({ + id: spell.spell_id, + name: spell.name ? System.decryptDataWithUserKey(spell.name, userKey) : '', + description: spell.description ? System.decryptDataWithUserKey(spell.description, userKey) : '', + tags: spell.tags ? JSON.parse(System.decryptDataWithUserKey(spell.tags, userKey)) : [] + })); + + const tags: SeriesSpellTagProps[] = tagsResult.map((tag): SeriesSpellTagProps => ({ + id: tag.tag_id, + name: tag.name ? System.decryptDataWithUserKey(tag.name, userKey) : '', + color: tag.color + })); + + return { spells, tags }; + } + + /** + * Retrieves the 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 details + */ + public static getSpellDetail(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellDetailProps { + const userKey: string = getUserEncryptionKey(userId); + const spell: SeriesSpellResult | null = SeriesSpellRepo.fetchSpellById(userId, spellId, lang); + + if (!spell) { + throw new Error(lang === 'fr' ? 'Sort non trouvé.' : 'Spell not found.'); + } + + return { + id: spell.spell_id, + name: spell.name ? System.decryptDataWithUserKey(spell.name, userKey) : '', + description: spell.description ? System.decryptDataWithUserKey(spell.description, userKey) : '', + appearance: spell.appearance ? System.decryptDataWithUserKey(spell.appearance, userKey) : '', + tags: spell.tags ? JSON.parse(System.decryptDataWithUserKey(spell.tags, userKey)) : [], + 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 + }; + } + + /** + * Adds a new spell to a series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param name - The spell name + * @param lang - The language for error messages ('fr' or 'en') + * @param description - The spell description + * @param appearance - The spell appearance + * @param tags - The spell tags + * @param powerLevel - The spell power level + * @param components - The spell components + * @param limitations - The spell limitations + * @param notes - The spell notes + * @returns The new spell ID + */ + public static addSpell(userId: string, seriesId: string, name: string, lang: 'fr' | 'en' = 'fr', description?: string | null, appearance?: string | null, tags?: string[], powerLevel?: string | null, components?: string | null, limitations?: string | null, notes?: string | null): string { + const userKey: string = getUserEncryptionKey(userId); + const spellId: string = System.createUniqueId(); + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + const nameHash: string = System.hashElement(name); + const encryptedDescription: string = description ? System.encryptDataWithUserKey(description, userKey) : ''; + const encryptedAppearance: string = appearance ? 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; + + SeriesSpellRepo.insertSpell(spellId, seriesId, userId, encryptedName, nameHash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, lang); + return spellId; + } + + /** + * Updates an existing spell. + * @param userId - The unique identifier of the user + * @param spellId - The unique identifier of the spell + * @param name - The spell name + * @param lang - The language for error messages ('fr' or 'en') + * @param description - The spell description + * @param appearance - The spell appearance + * @param tags - The spell tags + * @param powerLevel - The spell power level + * @param components - The spell components + * @param limitations - The spell limitations + * @param notes - The spell notes + * @returns True if successful + */ + public static updateSpell(userId: string, spellId: string, name: string, lang: 'fr' | 'en' = 'fr', description?: string | null, appearance?: string | null, tags?: string[], powerLevel?: string | null, components?: string | null, limitations?: string | null, notes?: string | null): boolean { + const userKey: string = getUserEncryptionKey(userId); + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + const nameHash: string = System.hashElement(name); + const encryptedDescription: string = description ? System.encryptDataWithUserKey(description, userKey) : ''; + const encryptedAppearance: string = appearance ? 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 SeriesSpellRepo.updateSpell(userId, spellId, encryptedName, nameHash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, lang); + } + + /** + * 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 successful + */ + public static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean { + return SeriesSpellRepo.deleteSpell(userId, spellId, lang); + } + + /** + * Adds a new tag to a series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param name - The name of the tag + * @param lang - The language for error messages ('fr' or 'en') + * @param color - The color of the tag (optional) + * @returns The new tag ID + */ + public static addTag(userId: string, seriesId: string, name: string, lang: 'fr' | 'en' = 'fr', color?: string | null): string { + const userKey: string = getUserEncryptionKey(userId); + const tagId: string = System.createUniqueId(); + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + const hashedName: string = System.hashElement(name); + + SeriesSpellRepo.insertTag(tagId, seriesId, userId, encryptedName, hashedName, color || null, lang); + return tagId; + } + + /** + * Updates an existing 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 lang - The language for error messages ('fr' or 'en') + * @param color - The new color of the tag (optional) + * @returns True if successful + */ + public static updateTag(userId: string, tagId: string, name: string, lang: 'fr' | 'en' = 'fr', color?: string | null): boolean { + const userKey: string = getUserEncryptionKey(userId); + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + const hashedName: string = System.hashElement(name); + + return SeriesSpellRepo.updateTag(userId, tagId, encryptedName, hashedName, color || null, lang); + } + + /** + * Deletes a tag. + * @param userId - The unique identifier of the user + * @param tagId - The unique identifier of the tag + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if successful + */ + public static deleteTag(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean { + return SeriesSpellRepo.deleteTag(userId, tagId, lang); + } +} diff --git a/electron/database/models/SeriesSync.ts b/electron/database/models/SeriesSync.ts new file mode 100644 index 0000000..5d294e8 --- /dev/null +++ b/electron/database/models/SeriesSync.ts @@ -0,0 +1,199 @@ +import { getUserEncryptionKey } from "../keyManager.js"; +import System from "../System.js"; +import SeriesSyncRepo, { SyncElementType } from "../repositories/series-sync.repo.js"; + +export interface SeriesSyncUploadPayload { + type: SyncElementType; + bookElementId: string; + field: string; + value: string; +} + +export interface SeriesSyncResult { + success: boolean; + updatedCount: number; +} + +export default class SeriesSync { + /** + * Uploads a field value from a book element to its linked series element, + * and propagates the change to all other book elements linked to the same series element. + * @param userId - The unique identifier of the user + * @param payload - The upload payload containing type, bookElementId, field, and value + * @param lang - The language for error messages ('fr' or 'en') + * @returns The upload response + */ + public static uploadFieldToSeries(userId: string, payload: SeriesSyncUploadPayload, lang: 'fr' | 'en' = 'fr'): SeriesSyncResult { + const { type, bookElementId, field, value } = payload; + + const seriesElementId: string | null = this.getSeriesLink(userId, type, bookElementId, lang); + if (!seriesElementId) { + throw new Error(lang === 'fr' ? `Cet élément n'est pas lié à une série.` : `This element is not linked to a series.`); + } + + const userKey: string = getUserEncryptionKey(userId); + const encryptedValue: string = value ? System.encryptDataWithUserKey(value, userKey) : ''; + + const dbField: string = this.mapFieldToDbColumn(type, field); + + this.updateSeriesElement(userId, type, seriesElementId, dbField, encryptedValue, lang); + const updatedCount: number = this.updateLinkedBookElements(userId, type, seriesElementId, dbField, encryptedValue, lang); + + return { + success: true, + updatedCount: updatedCount + 1 + }; + } + + /** + * Gets the series element ID linked to a book element. + * @param userId - The unique identifier of the user + * @param type - The type of element (character, world, location, spell) + * @param bookElementId - The unique identifier of the book element + * @param lang - The language for error messages ('fr' or 'en') + * @returns The series element ID or null if not linked + */ + private static getSeriesLink(userId: string, type: SyncElementType, bookElementId: string, lang: 'fr' | 'en'): string | null { + switch (type) { + case 'character': + return SeriesSyncRepo.getCharacterSeriesLink(userId, bookElementId, lang); + case 'world': + return SeriesSyncRepo.getWorldSeriesLink(userId, bookElementId, lang); + case 'location': + return SeriesSyncRepo.getLocationSeriesLink(userId, bookElementId, lang); + case 'spell': + return SeriesSyncRepo.getSpellSeriesLink(userId, bookElementId, lang); + } + } + + /** + * Maps frontend field names to database column names. + * @param type - The type of element (character, world, location, spell) + * @param field - The frontend field name to map + * @returns The corresponding database column name + */ + private static mapFieldToDbColumn(type: SyncElementType, field: string): string { + const characterFieldMap: Record = { + 'name': 'first_name', + 'firstName': 'first_name', + 'lastName': 'last_name', + 'nickname': 'nickname', + 'age': 'age', + 'gender': 'gender', + 'species': 'species', + 'nationality': 'nationality', + 'status': 'status', + 'title': 'title', + 'category': 'category', + 'role': 'role', + 'biography': 'biography', + 'history': 'history', + 'speechPattern': 'speech_pattern', + 'catchphrase': 'catchphrase', + 'residence': 'residence', + 'notes': 'notes', + 'color': 'color' + }; + + const worldFieldMap: Record = { + 'name': 'name', + 'history': 'history', + 'politics': 'politics', + 'economy': 'economy', + 'religion': 'religion', + 'languages': 'languages' + }; + + const locationFieldMap: Record = { + 'name': 'name', + 'loc_name': 'loc_name' + }; + + const spellFieldMap: Record = { + 'name': 'name', + 'description': 'description', + 'type': 'type', + 'level': 'level', + 'range': 'range', + 'duration': 'duration', + 'cost': 'cost', + 'effect': 'effect', + 'components': 'components', + 'notes': 'notes' + }; + + switch (type) { + case 'character': + return characterFieldMap[field] || field; + case 'world': + return worldFieldMap[field] || field; + case 'location': + return locationFieldMap[field] || field; + case 'spell': + return spellFieldMap[field] || field; + } + } + + /** + * Updates the series element field. + * @param userId - The unique identifier of the user + * @param type - The type of element (character, world, location, spell) + * @param seriesElementId - The unique identifier of the series element + * @param field - The database column name to update + * @param encryptedValue - The encrypted value to set + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if updated successfully + */ + private static updateSeriesElement(userId: string, type: SyncElementType, seriesElementId: string, field: string, encryptedValue: string, lang: 'fr' | 'en'): boolean { + switch (type) { + case 'character': + return SeriesSyncRepo.updateSeriesCharacterField(userId, seriesElementId, field, encryptedValue, lang); + case 'world': + return SeriesSyncRepo.updateSeriesWorldField(userId, seriesElementId, field, encryptedValue, lang); + case 'location': + return SeriesSyncRepo.updateSeriesLocationField(userId, seriesElementId, field, encryptedValue, lang); + case 'spell': + return SeriesSyncRepo.updateSeriesSpellField(userId, seriesElementId, field, encryptedValue, lang); + } + } + + /** + * Updates all book elements linked to the series element. + * @param userId - The unique identifier of the user + * @param type - The type of element (character, world, location, spell) + * @param seriesElementId - The unique identifier of the series element + * @param field - The database column name to update + * @param encryptedValue - The encrypted value to set + * @param lang - The language for error messages ('fr' or 'en') + * @returns The number of book elements updated + */ + private static updateLinkedBookElements(userId: string, type: SyncElementType, seriesElementId: string, field: string, encryptedValue: string, lang: 'fr' | 'en'): number { + const bookField: string = this.mapSeriesFieldToBookField(type, field); + + switch (type) { + case 'character': + return SeriesSyncRepo.updateLinkedBookCharactersField(userId, seriesElementId, bookField, encryptedValue, lang); + case 'world': + return SeriesSyncRepo.updateLinkedBookWorldsField(userId, seriesElementId, bookField, encryptedValue, lang); + case 'location': + return SeriesSyncRepo.updateLinkedBookLocationsField(userId, seriesElementId, bookField, encryptedValue, lang); + case 'spell': + return SeriesSyncRepo.updateLinkedBookSpellsField(userId, seriesElementId, bookField, encryptedValue, lang); + } + } + + /** + * Maps series table field names to book table field names (if different). + * @param type - The type of element (character, world, location, spell) + * @param seriesField - The series table field name + * @returns The corresponding book table field name + */ + private static mapSeriesFieldToBookField(type: SyncElementType, seriesField: string): string { + if (type === 'location') { + if (seriesField === 'name') { + return 'loc_name'; + } + } + return seriesField; + } +} diff --git a/electron/database/models/SeriesWorld.ts b/electron/database/models/SeriesWorld.ts new file mode 100644 index 0000000..86864f8 --- /dev/null +++ b/electron/database/models/SeriesWorld.ts @@ -0,0 +1,190 @@ +import { getUserEncryptionKey } from "../keyManager.js"; +import System from "../System.js"; +import SeriesWorldRepo, { SeriesWorldResult } from "../repositories/series-world.repo.js"; + +export interface SeriesWorldElementProps { + id: string; + name: string; + description: string; +} + +export interface SeriesWorldListProps { + id: string; + name: string; + history: string; + politics: string; + economy: string; + religion: string; + languages: string; + laws: SeriesWorldElementProps[]; + biomes: SeriesWorldElementProps[]; + issues: SeriesWorldElementProps[]; + customs: SeriesWorldElementProps[]; + kingdoms: SeriesWorldElementProps[]; + climate: SeriesWorldElementProps[]; + resources: SeriesWorldElementProps[]; + wildlife: SeriesWorldElementProps[]; + arts: SeriesWorldElementProps[]; + ethnicGroups: SeriesWorldElementProps[]; + socialClasses: SeriesWorldElementProps[]; + importantCharacters: SeriesWorldElementProps[]; +} + +export interface SeriesWorldUpdateProps { + name: string; + history?: string; + politics?: string; + economy?: string; + religion?: string; + languages?: string; +} + +const ELEMENT_TYPE_MAP: Record = { + 0: 'laws', + 1: 'biomes', + 2: 'issues', + 3: 'customs', + 4: 'kingdoms', + 5: 'climate', + 6: 'resources', + 7: 'wildlife', + 8: 'arts', + 9: 'ethnicGroups', + 10: 'socialClasses', + 11: 'importantCharacters' +}; + +export default class SeriesWorld { + /** + * Retrieves all worlds and their elements for a series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns The list of worlds + */ + public static getWorldList(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldListProps[] { + const userKey: string = getUserEncryptionKey(userId); + const worldsResult: SeriesWorldResult[] = SeriesWorldRepo.fetchWorlds(userId, seriesId, lang); + + const worldsMap: Map = new Map(); + + for (const row of worldsResult) { + if (!worldsMap.has(row.world_id)) { + worldsMap.set(row.world_id, { + id: row.world_id, + name: row.world_name ? System.decryptDataWithUserKey(row.world_name, userKey) : '', + history: row.history ? System.decryptDataWithUserKey(row.history, userKey) : '', + politics: row.politics ? System.decryptDataWithUserKey(row.politics, userKey) : '', + economy: row.economy ? System.decryptDataWithUserKey(row.economy, userKey) : '', + religion: row.religion ? System.decryptDataWithUserKey(row.religion, userKey) : '', + languages: row.languages ? System.decryptDataWithUserKey(row.languages, userKey) : '', + laws: [], + biomes: [], + issues: [], + customs: [], + kingdoms: [], + climate: [], + resources: [], + wildlife: [], + arts: [], + ethnicGroups: [], + socialClasses: [], + importantCharacters: [] + }); + } + + if (row.element_id) { + const world = worldsMap.get(row.world_id)!; + const element: SeriesWorldElementProps = { + id: row.element_id, + name: row.element_name ? System.decryptDataWithUserKey(row.element_name, userKey) : '', + description: row.element_description ? System.decryptDataWithUserKey(row.element_description, userKey) : '' + }; + + const key = ELEMENT_TYPE_MAP[row.element_type]; + if (key && Array.isArray(world[key])) { + (world[key] as SeriesWorldElementProps[]).push(element); + } + } + } + + return Array.from(worldsMap.values()); + } + + /** + * Adds a new world to a series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param name - The name of the world + * @param lang - The language for error messages ('fr' or 'en') + * @returns The new world ID + */ + public static addWorld(userId: string, seriesId: string, name: string, lang: 'fr' | 'en' = 'fr'): string { + const hashedName: string = System.hashElement(name); + + const exists: boolean = SeriesWorldRepo.checkWorldExist(userId, seriesId, hashedName, lang); + if (exists) { + throw new Error(lang === 'fr' ? 'Un monde avec ce nom existe déjà.' : 'A world with this name already exists.'); + } + + const userKey: string = getUserEncryptionKey(userId); + const worldId: string = System.createUniqueId(); + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + + SeriesWorldRepo.insertNewWorld(worldId, userId, seriesId, encryptedName, hashedName, lang); + return worldId; + } + + /** + * Updates a world's information. + * @param userId - The unique identifier of the user + * @param worldId - The unique identifier of the world + * @param world - The updated world data + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if successful + */ + public static updateWorld(userId: string, worldId: string, world: SeriesWorldUpdateProps, lang: 'fr' | 'en' = 'fr'): boolean { + const userKey: string = getUserEncryptionKey(userId); + const encryptedName: string = System.encryptDataWithUserKey(world.name, userKey); + const hashedName: string = System.hashElement(world.name); + const encryptedHistory: string | null = world.history ? System.encryptDataWithUserKey(world.history, userKey) : null; + const encryptedPolitics: string | null = world.politics ? System.encryptDataWithUserKey(world.politics, userKey) : null; + const encryptedEconomy: string | null = world.economy ? System.encryptDataWithUserKey(world.economy, userKey) : null; + const encryptedReligion: string | null = world.religion ? System.encryptDataWithUserKey(world.religion, userKey) : null; + const encryptedLanguages: string | null = world.languages ? System.encryptDataWithUserKey(world.languages, userKey) : null; + + return SeriesWorldRepo.updateWorld(userId, worldId, encryptedName, hashedName, encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, lang); + } + + /** + * Adds a new element to a world. + * @param userId - The unique identifier of the user + * @param worldId - The unique identifier of the world + * @param elementType - The type of element (0-11) + * @param name - The name of the element + * @param lang - The language for error messages ('fr' or 'en') + * @param description - The description of the element (optional) + * @returns The new element ID + */ + public static addElement(userId: string, worldId: string, elementType: number, name: string, lang: 'fr' | 'en' = 'fr', description?: string): string { + const userKey: string = getUserEncryptionKey(userId); + const elementId: string = System.createUniqueId(); + const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + const originalName: string = System.hashElement(name); + const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null; + + SeriesWorldRepo.insertElement(elementId, worldId, userId, elementType, encryptedName, originalName, encryptedDescription, lang); + return elementId; + } + + /** + * Deletes an element from a world. + * @param userId - The unique identifier of the user + * @param elementId - The unique identifier of the element + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if successful + */ + public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { + return SeriesWorldRepo.deleteElement(userId, elementId, lang); + } +} diff --git a/electron/database/models/Spell.ts b/electron/database/models/Spell.ts index 25454e4..dd95cb7 100644 --- a/electron/database/models/Spell.ts +++ b/electron/database/models/Spell.ts @@ -20,6 +20,7 @@ export interface SpellProps { components: string | null; limitations: string | null; notes: string | null; + seriesSpellId: string | null; } export interface SpellListItem { @@ -27,6 +28,7 @@ export interface SpellListItem { name: string; description: string; tags: SpellTagProps[]; + seriesSpellId?: string | null; } export interface SpellListResponse { @@ -193,6 +195,7 @@ export default class Spell { name: decryptedName, description: truncatedDescription, tags: resolvedTags, + seriesSpellId: spell.series_spell_id || null, }; }); @@ -240,6 +243,7 @@ export default class Spell { 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, }; } @@ -259,7 +263,7 @@ export default class Spell { * @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'): SpellProps { + 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(); @@ -287,6 +291,7 @@ export default class Spell { encryptedLimitations, encryptedNotes, lang, + seriesSpellId, ); return { @@ -299,6 +304,7 @@ export default class Spell { components, limitations, notes, + seriesSpellId, }; } @@ -317,7 +323,7 @@ export default class Spell { * @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'): boolean { + 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); @@ -343,6 +349,7 @@ export default class Spell { encryptedLimitations, encryptedNotes, lang, + seriesSpellId, ); } diff --git a/electron/database/models/Sync.ts b/electron/database/models/Sync.ts index e002227..78de3a6 100644 --- a/electron/database/models/Sync.ts +++ b/electron/database/models/Sync.ts @@ -1068,7 +1068,11 @@ export default class Sync { const bookToolsQuery: SyncedBookToolsResult | null = BookRepo.fetchSyncedBookTools(userId, currentBookId, lang); const bookTools: SyncedBookTools | null = bookToolsQuery ? { - lastUpdate: bookToolsQuery.last_update + lastUpdate: bookToolsQuery.last_update, + charactersEnabled: bookToolsQuery.characters_enabled === 1, + worldsEnabled: bookToolsQuery.worlds_enabled === 1, + locationsEnabled: bookToolsQuery.locations_enabled === 1, + spellsEnabled: bookToolsQuery.spells_enabled === 1 } : null; const bookSpells: SyncedSpell[] = allSpells diff --git a/electron/database/models/World.ts b/electron/database/models/World.ts index 06188b7..546d0dd 100644 --- a/electron/database/models/World.ts +++ b/electron/database/models/World.ts @@ -43,6 +43,7 @@ export interface WorldProps { ethnicGroups: WorldElement[]; socialClasses: WorldElement[]; importantCharacters: WorldElement[]; + seriesWorldId?: string | null; } export interface WorldListResponse { @@ -97,7 +98,7 @@ export default class World { * @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 { + 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)) { @@ -105,7 +106,7 @@ export default class World { } const encryptedWorldName: string = System.encryptDataWithUserKey(worldName, userEncryptionKey); const worldId: string = existingWorldId || System.createUniqueId(); - return WorldRepository.insertNewWorld(worldId, userId, bookId, encryptedWorldName, hashedWorldName, lang); + return WorldRepository.insertNewWorld(worldId, userId, bookId, encryptedWorldName, hashedWorldName, lang, seriesWorldId); } /** @@ -147,6 +148,7 @@ export default class World { ethnicGroups: [], socialClasses: [], importantCharacters: [], + seriesWorldId: queryRow.series_world_id || null, }; worlds.push(newWorld); @@ -228,7 +230,7 @@ export default class World { })); }); - WorldRepository.updateWorld(userId, world.id, encryptedName, System.hashElement(world.name), encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, System.timeStampInSeconds(), lang); + 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); } diff --git a/electron/database/repositories/book.repository.ts b/electron/database/repositories/book.repository.ts index d857428..b01026d 100644 --- a/electron/database/repositories/book.repository.ts +++ b/electron/database/repositories/book.repository.ts @@ -58,6 +58,10 @@ export interface BookToolsTable extends Record { export interface SyncedBookToolsResult extends Record { last_update: number; + characters_enabled: number; + worlds_enabled: number; + locations_enabled: number; + spells_enabled: number; } export default class BookRepo { @@ -440,7 +444,7 @@ export default class BookRepo { static fetchSyncedBookTools(userId: string, bookId: string, lang: 'fr' | 'en'): SyncedBookToolsResult | null { try { const db: Database = System.getDb(); - const query: string = 'SELECT last_update FROM book_tools WHERE user_id = ? AND book_id = ?'; + const query: string = 'SELECT last_update, characters_enabled, worlds_enabled, locations_enabled, spells_enabled FROM book_tools WHERE user_id = ? AND book_id = ?'; const params: SQLiteValue[] = [userId, bookId]; const result = db.get(query, params) as SyncedBookToolsResult | undefined; return result ?? null; diff --git a/electron/database/repositories/chaptercontent.repository.ts b/electron/database/repositories/chaptercontent.repository.ts index e0e8ef5..17cdacb 100644 --- a/electron/database/repositories/chaptercontent.repository.ts +++ b/electron/database/repositories/chaptercontent.repository.ts @@ -63,8 +63,7 @@ export default class ChapterContentRepository { LIMIT 1 `; const params: SQLiteValue[] = [userId, bookId]; - const chapterContents: ChapterContentQueryResult[] = db.all(query, params) as ChapterContentQueryResult[]; - return chapterContents; + return db.all(query, params) as ChapterContentQueryResult[]; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); @@ -127,8 +126,7 @@ export default class ChapterContentRepository { const db: Database = System.getDb(); const query: string = 'SELECT version, content, words_count FROM book_chapter_content WHERE author_id=? AND chapter_id=? AND version=?'; const params: SQLiteValue[] = [userId, chapterId, version]; - const companionContents: CompanionContentQueryResult[] = db.all(query, params) as CompanionContentQueryResult[]; - return companionContents; + return db.all(query, params) as CompanionContentQueryResult[]; } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); diff --git a/electron/database/repositories/character.repository.ts b/electron/database/repositories/character.repository.ts index 7e4e5ba..34fef4f 100644 --- a/electron/database/repositories/character.repository.ts +++ b/electron/database/repositories/character.repository.ts @@ -54,23 +54,24 @@ export interface CharacterResult extends Record { character_id: string; first_name: string; last_name: string; - nickname: string; - age: string; - gender: string; - species: string; - nationality: string; - status: string; + nickname: string | null; + age: string | null; + gender: string | null; + species: string | null; + nationality: string | null; + status: string | null; title: string; category: string; image: string; role: string; biography: string; history: string; - speech_pattern: string; - catchphrase: string; - residence: string; - notes: string; - color: string; + speech_pattern: string | null; + catchphrase: string | null; + residence: string | null; + notes: string | null; + color: string | null; + series_character_id: string | null; } export interface AttributeResult extends Record { @@ -83,11 +84,22 @@ export interface CompleteCharacterResult extends Record { character_id: string; first_name: string; last_name: string; + nickname: string | null; + age: string | null; + gender: string | null; + species: string | null; + nationality: string | null; + status: string | null; category: string; title: string; role: string; biography: string; history: string; + speech_pattern: string | null; + catchphrase: string | null; + residence: string | null; + notes: string | null; + color: string | null; attribute_name: string; attribute_value: string; } @@ -103,7 +115,7 @@ export default class CharacterRepo { public static fetchCharacters(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): CharacterResult[] { try { const db: Database = System.getDb(); - const query: string = 'SELECT character_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color FROM book_characters WHERE book_id=? AND user_id=?'; + const query: string = 'SELECT character_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, series_character_id FROM book_characters WHERE book_id=? AND user_id=?'; const params: SQLiteValue[] = [bookId, userId]; const characters: CharacterResult[] = db.all(query, params) as CharacterResult[]; return characters; @@ -129,46 +141,35 @@ export default class CharacterRepo { */ public static addNewCharacter(userId: string, characterId: string, characterData: { firstName: string; - lastName: string; - nickname: string; - age: string; - gender: string; - species: string; - nationality: string; - status: string; - title: string; - category: string; - image: string; - role: string; - biography: string; - history: string; - speechPattern: string; - catchphrase: string; - residence: string; - notes: string; - color: string; - }, bookId: string, lang: 'fr' | 'en' = 'fr'): string { + lastName: string | null; + nickname: string | null; + age: string | null; + gender: string | null; + species: string | null; + nationality: string | null; + status: string | null; + title: string | null; + category: string | null; + image: string | null; + role: string | null; + biography: string | null; + history: string | null; + speechPattern: string | null; + catchphrase: string | null; + residence: string | null; + notes: string | null; + color: string | null; + }, bookId: string, lang: 'fr' | 'en' = 'fr', seriesCharacterId: string | null = null): string { + let insertResult: RunResult; try { const db: Database = System.getDb(); - const query: string = `INSERT INTO book_characters ( - character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, - category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`; - const params: SQLiteValue[] = [ - characterId, bookId, userId, - characterData.firstName, characterData.lastName, characterData.nickname, - characterData.age, characterData.gender, characterData.species, - characterData.nationality, characterData.status, characterData.category, - characterData.title, characterData.image, characterData.role, - characterData.biography, characterData.history, characterData.speechPattern, - characterData.catchphrase, characterData.residence, characterData.notes, - characterData.color, System.timeStampInSeconds() - ]; - const insertResult: RunResult = db.run(query, params); - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du personnage.` : `Error adding character.`); - } - return characterId; + const query: string = seriesCharacterId + ? 'INSERT INTO book_characters (character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, series_character_id, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)' + : 'INSERT INTO book_characters (character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'; + const params: SQLiteValue[] = seriesCharacterId + ? [characterId, bookId, userId, characterData.firstName, characterData.lastName, characterData.nickname, characterData.age, characterData.gender, characterData.species, characterData.nationality, characterData.status, characterData.category, characterData.title, characterData.image, characterData.role, characterData.biography, characterData.history, characterData.speechPattern, characterData.catchphrase, characterData.residence, characterData.notes, characterData.color, seriesCharacterId, System.timeStampInSeconds()] + : [characterId, bookId, userId, characterData.firstName, characterData.lastName, characterData.nickname, characterData.age, characterData.gender, characterData.species, characterData.nationality, characterData.status, characterData.category, characterData.title, characterData.image, characterData.role, characterData.biography, characterData.history, characterData.speechPattern, characterData.catchphrase, characterData.residence, characterData.notes, characterData.color, System.timeStampInSeconds()]; + insertResult = db.run(query, params); } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); @@ -178,6 +179,10 @@ export default class CharacterRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du personnage.` : `Error adding character.`); + } + return characterId; } /** @@ -191,15 +196,12 @@ export default class CharacterRepo { * @returns The attribute ID if successful */ static insertAttribute(attributeId: string, characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; try { const db: Database = System.getDb(); - const query: string = 'INSERT INTO `book_characters_attributes` (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?,?,?,?,?,?)'; + const query: string = 'INSERT INTO book_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?,?,?,?,?,?)'; const params: SQLiteValue[] = [attributeId, characterId, userId, type, name, System.timeStampInSeconds()]; - const insertResult: RunResult = db.run(query, params); - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'attribut.` : `Error adding attribute.`); - } - return attributeId; + insertResult = db.run(query, params); } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); @@ -209,6 +211,10 @@ export default class CharacterRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'attribut.` : `Error adding attribute.`); + } + return attributeId; } /** @@ -222,41 +228,33 @@ export default class CharacterRepo { */ static updateCharacter(userId: string, id: string, characterData: { firstName: string; - lastName: string; - nickname: string; - age: string; - gender: string; - species: string; - nationality: string; - status: string; - title: string; - category: string; - image: string; - role: string; - biography: string; - history: string; - speechPattern: string; - catchphrase: string; - residence: string; - notes: string; - color: string; - }, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + lastName: string | null; + nickname: string | null; + age: string | null; + gender: string | null; + species: string | null; + nationality: string | null; + status: string | null; + title: string | null; + category: string | null; + image: string | null; + role: string | null; + biography: string | null; + history: string | null; + speechPattern: string | null; + catchphrase: string | null; + residence: string | null; + notes: string | null; + color: string | null; + }, lastUpdate: number, lang: 'fr' | 'en' = 'fr', seriesCharacterId: string | null = null): boolean { try { const db: Database = System.getDb(); - const query: string = `UPDATE book_characters SET - first_name=?, last_name=?, nickname=?, age=?, gender=?, species=?, nationality=?, status=?, - title=?, category=?, image=?, role=?, biography=?, history=?, - speech_pattern=?, catchphrase=?, residence=?, notes=?, color=?, last_update=? - WHERE character_id=? AND user_id=?`; - const params: SQLiteValue[] = [ - characterData.firstName, characterData.lastName, characterData.nickname, - characterData.age, characterData.gender, characterData.species, - characterData.nationality, characterData.status, characterData.title, - characterData.category, characterData.image, characterData.role, - characterData.biography, characterData.history, characterData.speechPattern, - characterData.catchphrase, characterData.residence, characterData.notes, - characterData.color, lastUpdate, id, userId - ]; + const query: string = seriesCharacterId !== null + ? 'UPDATE book_characters SET first_name=?, last_name=?, nickname=?, age=?, gender=?, species=?, nationality=?, status=?, title=?, category=?, image=?, role=?, biography=?, history=?, speech_pattern=?, catchphrase=?, residence=?, notes=?, color=?, series_character_id=?, last_update=? WHERE character_id=? AND user_id=?' + : 'UPDATE book_characters SET first_name=?, last_name=?, nickname=?, age=?, gender=?, species=?, nationality=?, status=?, title=?, category=?, image=?, role=?, biography=?, history=?, speech_pattern=?, catchphrase=?, residence=?, notes=?, color=?, last_update=? WHERE character_id=? AND user_id=?'; + const params: SQLiteValue[] = seriesCharacterId !== null + ? [characterData.firstName, characterData.lastName, characterData.nickname, characterData.age, characterData.gender, characterData.species, characterData.nationality, characterData.status, characterData.title, characterData.category, characterData.image, characterData.role, characterData.biography, characterData.history, characterData.speechPattern, characterData.catchphrase, characterData.residence, characterData.notes, characterData.color, seriesCharacterId, lastUpdate, id, userId] + : [characterData.firstName, characterData.lastName, characterData.nickname, characterData.age, characterData.gender, characterData.species, characterData.nationality, characterData.status, characterData.title, characterData.category, characterData.image, characterData.role, characterData.biography, characterData.history, characterData.speechPattern, characterData.catchphrase, characterData.residence, characterData.notes, characterData.color, lastUpdate, id, userId]; const updateResult: RunResult = db.run(query, params); return updateResult.changes > 0; } catch (error: unknown) { @@ -357,7 +355,7 @@ export default class CharacterRepo { static fetchCompleteCharacters(userId: string, bookId: string, tags: string[], lang: 'fr' | 'en' = 'fr'): CompleteCharacterResult[] { try { const db: Database = System.getDb(); - let query: string = 'SELECT charac.character_id, first_name, last_name, category, title, role, biography, history, attribute_name, attribute_value FROM book_characters AS charac LEFT JOIN book_characters_attributes AS attr ON charac.character_id=attr.character_id WHERE charac.user_id=? AND charac.book_id=?'; + let query: string = 'SELECT charac.character_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, role, biography, history, speech_pattern, catchphrase, residence, notes, color, attribute_name, attribute_value FROM book_characters AS charac LEFT JOIN book_characters_attributes AS attr ON charac.character_id=attr.character_id WHERE charac.user_id=? AND charac.book_id=?'; let params: SQLiteValue[] = [userId, bookId]; if (tags && tags.length > 0) { const placeholders: string = tags.map((): string => '?').join(','); @@ -393,7 +391,7 @@ export default class CharacterRepo { static updateCharacterAttribute(userId: string, characterAttributeId: string, attributeName: string, attributeValue: string, lastUpdate: number, lang: "fr" | "en"): boolean { try { const db: Database = System.getDb(); - const query: string = 'UPDATE `book_characters_attributes` SET `attribute_name`=?,`attribute_value`=?, last_update=FROM_UNIXTIME(?) WHERE `attr_id`=UUID_TO_BIN(?) AND `user_id`=UUID_TO_BIN(?)'; + const query: string = 'UPDATE book_characters_attributes SET attribute_name=?, attribute_value=?, last_update=? WHERE attr_id=? AND user_id=?'; const params: SQLiteValue[] = [attributeName, attributeValue, lastUpdate, characterAttributeId, userId]; const updateResult: RunResult = db.run(query, params); return updateResult.changes > 0; diff --git a/electron/database/repositories/location.repository.ts b/electron/database/repositories/location.repository.ts index 33a9d2b..152fe62 100644 --- a/electron/database/repositories/location.repository.ts +++ b/electron/database/repositories/location.repository.ts @@ -10,6 +10,7 @@ export interface LocationQueryResult extends Record { sub_element_id: string; sub_elem_name: string; sub_elem_description: string; + series_location_id: string | null; } export interface LocationElementQueryResult extends Record { @@ -89,15 +90,7 @@ export default class LocationRepo { static getLocation(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LocationQueryResult[] { try { const db: Database = System.getDb(); - const query: string = ` - SELECT loc_id, loc_name, element.element_id AS element_id, element.element_name, - element.element_description, sub_elem.sub_element_id AS sub_element_id, - sub_elem.sub_elem_name, sub_elem.sub_elem_description - FROM book_location AS location - LEFT JOIN location_element AS element ON location.loc_id = element.location - LEFT JOIN location_sub_element AS sub_elem ON element.element_id = sub_elem.element_id - WHERE location.user_id = ? AND location.book_id = ? - `; + const query: string = 'SELECT loc_id, loc_name, element.element_id AS element_id, element.element_name, element.element_description, sub_elem.sub_element_id AS sub_element_id, sub_elem.sub_elem_name, sub_elem.sub_elem_description, location.series_location_id FROM book_location AS location LEFT JOIN location_element AS element ON location.loc_id = element.location LEFT JOIN location_sub_element AS sub_elem ON element.element_id = sub_elem.element_id WHERE location.user_id = ? AND location.book_id = ?'; const params: SQLiteValue[] = [userId, bookId]; const locations: LocationQueryResult[] = db.all(query, params) as LocationQueryResult[]; return locations; @@ -122,19 +115,17 @@ export default class LocationRepo { * @param lang - The language for error messages ('fr' or 'en') * @returns The location ID if insertion was successful */ - static insertLocation(userId: string, locationId: string, bookId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): string { + static insertLocation(userId: string, locationId: string, bookId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr', seriesLocationId: string | null = null): string { + let insertResult: RunResult; try { const db: Database = System.getDb(); - const query: string = ` - INSERT INTO book_location (loc_id, book_id, user_id, loc_name, loc_original_name, last_update) - VALUES (?, ?, ?, ?, ?, ?) - `; - const params: SQLiteValue[] = [locationId, bookId, userId, encryptedName, originalName, System.timeStampInSeconds()]; - const insertResult: RunResult = db.run(query, params); - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de la section d'emplacement.` : `Error adding location section.`); - } - return locationId; + const query: string = seriesLocationId + ? 'INSERT INTO book_location (loc_id, book_id, user_id, loc_name, loc_original_name, series_location_id, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)' + : 'INSERT INTO book_location (loc_id, book_id, user_id, loc_name, loc_original_name, last_update) VALUES (?, ?, ?, ?, ?, ?)'; + const params: SQLiteValue[] = seriesLocationId + ? [locationId, bookId, userId, encryptedName, originalName, seriesLocationId, System.timeStampInSeconds()] + : [locationId, bookId, userId, encryptedName, originalName, System.timeStampInSeconds()]; + insertResult = db.run(query, params); } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); @@ -144,6 +135,10 @@ export default class LocationRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de la section d'emplacement.` : `Error adding location section.`); + } + return locationId; } /** @@ -157,18 +152,12 @@ export default class LocationRepo { * @returns The element ID if insertion was successful */ static insertLocationElement(userId: string, elementId: string, locationId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; try { const db: Database = System.getDb(); - const query: string = ` - INSERT INTO location_element (element_id, location, user_id, element_name, original_name, element_description, last_update) - VALUES (?, ?, ?, ?, ?, ?, ?) - `; + const query: string = 'INSERT INTO location_element (element_id, location, user_id, element_name, original_name, element_description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)'; const params: SQLiteValue[] = [elementId, locationId, userId, encryptedName, originalName, '', System.timeStampInSeconds()]; - const insertResult: RunResult = db.run(query, params); - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'élément d'emplacement.` : `Error adding location element.`); - } - return elementId; + insertResult = db.run(query, params); } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); @@ -178,6 +167,10 @@ export default class LocationRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'élément d'emplacement.` : `Error adding location element.`); + } + return elementId; } /** @@ -191,18 +184,12 @@ export default class LocationRepo { * @returns The sub-element ID if insertion was successful */ static insertLocationSubElement(userId: string, subElementId: string, elementId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; try { const db: Database = System.getDb(); - const query: string = ` - INSERT INTO location_sub_element (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) - VALUES (?, ?, ?, ?, ?, ?, ?) - `; + const query: string = 'INSERT INTO location_sub_element (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)'; const params: SQLiteValue[] = [subElementId, elementId, userId, encryptedName, originalName, '', System.timeStampInSeconds()]; - const insertResult: RunResult = db.run(query, params); - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du sous-élément d'emplacement.` : `Error adding location sub-element.`); - } - return subElementId; + insertResult = db.run(query, params); } catch (error: unknown) { if (error instanceof Error) { console.error(`DB Error: ${error.message}`); @@ -212,6 +199,10 @@ export default class LocationRepo { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du sous-élément d'emplacement.` : `Error adding location sub-element.`); + } + return subElementId; } /** @@ -864,4 +855,46 @@ export default class LocationRepo { } } } + + /** + * Updates a location section with optional name change and series link. + * @param userId - The user's unique identifier + * @param sectionId - The section's unique identifier + * @param encryptedName - The new encrypted name (optional) + * @param originalName - The new original name (optional) + * @param seriesLocationId - The series location ID to link (optional, null to unlink) + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the update was successful + */ + static updateSectionWithSeriesLink(userId: string, sectionId: string, encryptedName: string | null, originalName: string | null, seriesLocationId: string | null, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const setClauses: string[] = ['last_update=' + System.timeStampInSeconds()]; + const params: SQLiteValue[] = []; + + if (encryptedName !== null && originalName !== null) { + setClauses.push('loc_name=?', 'loc_original_name=?'); + params.push(encryptedName, originalName); + } + + if (seriesLocationId !== undefined) { + setClauses.push('series_location_id=?'); + params.push(seriesLocationId); + } + + params.push(sectionId, userId); + + const query: string = 'UPDATE book_location SET ' + setClauses.join(', ') + ' WHERE loc_id=? AND user_id=?'; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour la section d'emplacement.` : `Unable to update location section.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } } diff --git a/electron/database/repositories/series-character.repo.ts b/electron/database/repositories/series-character.repo.ts new file mode 100644 index 0000000..b9c1c51 --- /dev/null +++ b/electron/database/repositories/series-character.repo.ts @@ -0,0 +1,472 @@ +import {Database, QueryResult, RunResult, SQLiteValue} from 'node-sqlite3-wasm'; +import System from "../System.js"; + +export interface SeriesCharacterResult extends Record { + character_id: string; + first_name: string; + last_name: string; + nickname: string | null; + age: string | null; + gender: string | null; + species: string | null; + nationality: string | null; + status: string | null; + title: string; + category: string; + image: string; + role: string; + biography: string; + history: string; + speech_pattern: string | null; + catchphrase: string | null; + residence: string | null; + notes: string | null; + color: string | null; +} + +export interface SeriesCharacterAttributeResult extends Record { + attr_id: string; + attribute_name: string; + attribute_value: string; +} + +export interface SeriesCharactersTableResult extends Record { + character_id: string; + series_id: string; + user_id: string; + first_name: string; + last_name: string | null; + nickname: string | null; + age: string | null; + gender: string | null; + species: string | null; + nationality: string | null; + status: string | null; + title: string | null; + category: string; + image: string | null; + role: string | null; + biography: string | null; + history: string | null; + speech_pattern: string | null; + catchphrase: string | null; + residence: string | null; + notes: string | null; + color: string | null; + last_update: number; +} + +export interface SeriesCharacterAttributesTableResult extends Record { + attr_id: string; + character_id: string; + user_id: string; + attribute_name: string; + attribute_value: string; + last_update: number; +} + +export interface SyncedSeriesCharacterResult extends Record { + character_id: string; + series_id: string; + first_name: string; + last_update: number; +} + +export interface SyncedSeriesCharacterAttributeResult extends Record { + attr_id: string; + character_id: string; + attribute_name: string; + last_update: number; +} + +export default class SeriesCharacterRepo { + /** + * Fetches all characters for a specific series owned by the user. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns An array of character results + */ + public static fetchCharacters(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT character_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color FROM series_characters WHERE series_id = ? AND user_id = ?'; + return db.all(query, [seriesId, userId]) as SeriesCharacterResult[]; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages de la série.` : `Unable to retrieve series characters.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Adds a new character to a series. + */ + public static addNewCharacter(userId: string, characterId: string, encryptedName: string, encryptedLastName: string | null, encryptedNickname: string | null, encryptedAge: string | null, encryptedGender: string | null, encryptedSpecies: string | null, encryptedNationality: string | null, encryptedStatus: string | null, encryptedTitle: string | null, encryptedCategory: string | null, encryptedImage: string | null, encryptedRole: string | null, encryptedBiography: string | null, encryptedHistory: string | null, encryptedSpeechPattern: string | null, encryptedCatchphrase: string | null, encryptedResidence: string | null, encryptedNotes: string | null, encryptedColor: string | null, seriesId: string, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_characters (character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + const params: SQLiteValue[] = [characterId, seriesId, userId, encryptedName, encryptedLastName, encryptedNickname, encryptedAge, encryptedGender, encryptedSpecies, encryptedNationality, encryptedStatus, encryptedCategory, encryptedTitle, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, encryptedSpeechPattern, encryptedCatchphrase, encryptedResidence, encryptedNotes, encryptedColor, System.timeStampInSeconds()]; + insertResult = db.run(query, params); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter le personnage.` : `Unable to add character.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du personnage.` : `Error adding character.`); + } + return characterId; + } + + /** + * Inserts a new attribute for a series character. + */ + static insertAttribute(attributeId: string, characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?, ?, ?, ?, ?, ?)'; + const params: SQLiteValue[] = [attributeId, characterId, userId, type, name, System.timeStampInSeconds()]; + insertResult = db.run(query, params); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter l'attribut.` : `Unable to add attribute.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'attribut.` : `Error adding attribute.`); + } + return attributeId; + } + + /** + * Updates an existing series character's information. + */ + static updateCharacter(userId: string, characterId: string, encryptedName: string, encryptedLastName: string | null, encryptedNickname: string | null, encryptedAge: string | null, encryptedGender: string | null, encryptedSpecies: string | null, encryptedNationality: string | null, encryptedStatus: string | null, encryptedTitle: string | null, encryptedCategory: string | null, encryptedImage: string | null, encryptedRole: string | null, encryptedBiography: string | null, encryptedHistory: string | null, encryptedSpeechPattern: string | null, encryptedCatchphrase: string | null, encryptedResidence: string | null, encryptedNotes: string | null, encryptedColor: string | null, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_characters SET first_name = ?, last_name = ?, nickname = ?, age = ?, gender = ?, species = ?, nationality = ?, status = ?, title = ?, category = ?, image = ?, role = ?, biography = ?, history = ?, speech_pattern = ?, catchphrase = ?, residence = ?, notes = ?, color = ?, last_update = ? WHERE character_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [encryptedName, encryptedLastName, encryptedNickname, encryptedAge, encryptedGender, encryptedSpecies, encryptedNationality, encryptedStatus, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, encryptedSpeechPattern, encryptedCatchphrase, encryptedResidence, encryptedNotes, encryptedColor, System.timeStampInSeconds(), characterId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le personnage.` : `Unable to update character.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Deletes a series character and all its related data via CASCADE. + */ + static deleteCharacter(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + // Delete attributes first + db.run('DELETE FROM series_characters_attributes WHERE character_id = ? AND user_id = ?', [characterId, userId]); + // Delete character + const query: string = 'DELETE FROM series_characters WHERE character_id = ? AND user_id = ?'; + const deleteResult: RunResult = db.run(query, [characterId, userId]); + return deleteResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de supprimer le personnage.` : `Unable to delete character.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Deletes an attribute from a series character. + */ + static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'DELETE FROM series_characters_attributes WHERE attr_id = ? AND user_id = ?'; + const deleteResult: RunResult = db.run(query, [attributeId, userId]); + return deleteResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de supprimer l'attribut.` : `Unable to delete attribute.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all attributes for a specific series character. + */ + static fetchAttributes(characterId: string, userId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterAttributeResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT attr_id, attribute_name, attribute_value FROM series_characters_attributes WHERE character_id = ? AND user_id = ?'; + const attributes: SeriesCharacterAttributeResult[] = db.all(query, [characterId, userId]) as SeriesCharacterAttributeResult[]; + return attributes; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs.` : `Unable to retrieve attributes.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Checks if a series character exists. + */ + static isCharacterExist(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT 1 FROM series_characters WHERE character_id = ? AND user_id = ?'; + const result: QueryResult | null = db.get(query, [characterId, userId]); + return result !== null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du personnage.` : `Unable to check character existence.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all characters for a series for sync. + */ + static fetchSeriesCharactersTable(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharactersTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE series_id = ? AND user_id = ?'; + const characters: SeriesCharactersTableResult[] = db.all(query, [seriesId, userId]) as SeriesCharactersTableResult[]; + return characters; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages pour sync.` : `Unable to retrieve characters for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all attributes for a character for sync. + */ + static fetchSeriesCharacterAttributesTable(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterAttributesTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM series_characters_attributes WHERE character_id = ? AND user_id = ?'; + const attributes: SeriesCharacterAttributesTableResult[] = db.all(query, [characterId, userId]) as SeriesCharacterAttributesTableResult[]; + return attributes; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs pour sync.` : `Unable to retrieve attributes for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all series characters for a user for sync comparison. + */ + static fetchSyncedSeriesCharacters(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesCharacterResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT character_id, series_id, first_name, last_update FROM series_characters WHERE user_id = ?'; + const characters: SyncedSeriesCharacterResult[] = db.all(query, [userId]) as SyncedSeriesCharacterResult[]; + return characters; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages de série pour sync.` : `Unable to retrieve series characters for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all series character attributes for a user for sync comparison. + */ + static fetchSyncedSeriesCharacterAttributes(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesCharacterAttributeResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT attr_id, character_id, attribute_name, last_update FROM series_characters_attributes WHERE user_id = ?'; + const attributes: SyncedSeriesCharacterAttributeResult[] = db.all(query, [userId]) as SyncedSeriesCharacterAttributeResult[]; + return attributes; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs de personnage pour sync.` : `Unable to retrieve character attributes for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a complete character by ID for sync. + */ + static fetchCompleteCharacterById(characterId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharactersTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE character_id = ?'; + const characters: SeriesCharactersTableResult[] = db.all(query, [characterId]) as SeriesCharactersTableResult[]; + return characters; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le personnage complet.` : `Unable to retrieve complete character.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a complete character attribute by ID for sync. + */ + static fetchCompleteAttributeById(attrId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterAttributesTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM series_characters_attributes WHERE attr_id = ?'; + const attributes: SeriesCharacterAttributesTableResult[] = db.all(query, [attrId]) as SeriesCharacterAttributesTableResult[]; + return attributes; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer l'attribut complet.` : `Unable to retrieve complete attribute.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a series character for sync. + */ + static insertSyncSeriesCharacter(characterId: string, seriesId: string, userId: string, firstName: string, lastName: string | null, nickname: string | null, age: string | null, gender: string | null, species: string | null, nationality: string | null, status: string | null, category: string, title: string | null, image: string | null, role: string | null, biography: string | null, history: string | null, speechPattern: string | null, catchphrase: string | null, residence: string | null, notes: string | null, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_characters (character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(character_id) DO UPDATE SET first_name = excluded.first_name, last_name = excluded.last_name, nickname = excluded.nickname, age = excluded.age, gender = excluded.gender, species = excluded.species, nationality = excluded.nationality, status = excluded.status, category = excluded.category, title = excluded.title, image = excluded.image, role = excluded.role, biography = excluded.biography, history = excluded.history, speech_pattern = excluded.speech_pattern, catchphrase = excluded.catchphrase, residence = excluded.residence, notes = excluded.notes, color = excluded.color, last_update = excluded.last_update'; + const params: SQLiteValue[] = [characterId, seriesId, userId, firstName, lastName, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speechPattern, catchphrase, residence, notes, color, lastUpdate]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le personnage pour sync.` : `Unable to insert character for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates a series character for sync. + */ + static updateSyncSeriesCharacter(userId: string, characterId: string, firstName: string, lastName: string | null, nickname: string | null, age: string | null, gender: string | null, species: string | null, nationality: string | null, status: string | null, category: string, title: string | null, image: string | null, role: string | null, biography: string | null, history: string | null, speechPattern: string | null, catchphrase: string | null, residence: string | null, notes: string | null, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_characters SET first_name = ?, last_name = ?, nickname = ?, age = ?, gender = ?, species = ?, nationality = ?, status = ?, category = ?, title = ?, image = ?, role = ?, biography = ?, history = ?, speech_pattern = ?, catchphrase = ?, residence = ?, notes = ?, color = ?, last_update = ? WHERE character_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [firstName, lastName, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speechPattern, catchphrase, residence, notes, color, lastUpdate, characterId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le personnage pour sync.` : `Unable to update character for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a series character attribute for sync. + */ + static insertSyncSeriesCharacterAttribute(attrId: string, characterId: string, userId: string, attributeName: string, attributeValue: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(attr_id) DO UPDATE SET attribute_name = excluded.attribute_name, attribute_value = excluded.attribute_value, last_update = excluded.last_update'; + const params: SQLiteValue[] = [attrId, characterId, userId, attributeName, attributeValue, lastUpdate]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer l'attribut pour sync.` : `Unable to insert attribute for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Checks if a series character attribute exists. + */ + static isAttributeExist(userId: string, attrId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT 1 FROM series_characters_attributes WHERE attr_id = ? AND user_id = ?'; + const result: QueryResult | null = db.get(query, [attrId, userId]); + return result !== null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'attribut.` : `Unable to check attribute existence.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates a series character attribute for sync. + */ + static updateSyncSeriesCharacterAttribute(userId: string, attrId: string, attributeName: string, attributeValue: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_characters_attributes SET attribute_name = ?, attribute_value = ?, last_update = ? WHERE attr_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [attributeName, attributeValue, lastUpdate, attrId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'attribut pour sync.` : `Unable to update attribute for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } +} diff --git a/electron/database/repositories/series-location.repo.ts b/electron/database/repositories/series-location.repo.ts new file mode 100644 index 0000000..180ce39 --- /dev/null +++ b/electron/database/repositories/series-location.repo.ts @@ -0,0 +1,623 @@ +import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; +import System from "../System.js"; + +export interface SeriesLocationResult extends Record { + loc_id: string; + loc_name: string; +} + +export interface SeriesLocationElementResult extends Record { + element_id: string; + location_id: string; + element_name: string; + element_description: string; +} + +export interface SeriesLocationSubElementResult extends Record { + sub_element_id: string; + element_id: string; + sub_elem_name: string; + sub_elem_description: string; +} + +export interface SeriesLocationsTableResult extends Record { + loc_id: string; + series_id: string; + user_id: string; + loc_name: string; + loc_original_name: string; + last_update: number; +} + +export interface SeriesLocationElementsTableResult extends Record { + element_id: string; + location_id: string; + user_id: string; + element_name: string; + original_name: string; + element_description: string | null; + last_update: number; +} + +export interface SeriesLocationSubElementsTableResult extends Record { + sub_element_id: string; + element_id: string; + user_id: string; + sub_elem_name: string; + original_name: string; + sub_elem_description: string | null; + last_update: number; +} + +export interface SyncedSeriesLocationResult extends Record { + loc_id: string; + series_id: string; + loc_name: string; + last_update: number; +} + +export interface SyncedSeriesLocationElementResult extends Record { + element_id: string; + location_id: string; + element_name: string; + last_update: number; +} + +export interface SyncedSeriesLocationSubElementResult extends Record { + sub_element_id: string; + element_id: string; + sub_elem_name: string; + last_update: number; +} + +export default class SeriesLocationRepo { + /** + * Fetches all locations for a series. + */ + public static fetchLocations(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT loc_id, loc_name FROM series_locations WHERE user_id = ? AND series_id = ?'; + const locations: SeriesLocationResult[] = db.all(query, [userId, seriesId]) as SeriesLocationResult[]; + return locations; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les lieux.` : `Unable to retrieve locations.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all elements for a location. + */ + public static fetchElements(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationElementResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT element_id, location_id, element_name, element_description FROM series_location_elements WHERE user_id = ? AND location_id = ?'; + const elements: SeriesLocationElementResult[] = db.all(query, [userId, locationId]) as SeriesLocationElementResult[]; + return elements; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments.` : `Unable to retrieve elements.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all sub-elements for an element. + */ + public static fetchSubElements(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationSubElementResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT sub_element_id, element_id, sub_elem_name, sub_elem_description FROM series_location_sub_elements WHERE user_id = ? AND element_id = ?'; + const subElements: SeriesLocationSubElementResult[] = db.all(query, [userId, elementId]) as SeriesLocationSubElementResult[]; + return subElements; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments.` : `Unable to retrieve sub-elements.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a new location section. + */ + public static insertLocation(locationId: string, seriesId: string, userId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_locations (loc_id, series_id, user_id, loc_name, loc_original_name, last_update) VALUES (?, ?, ?, ?, ?, ?)'; + insertResult = db.run(query, [locationId, seriesId, userId, encryptedName, originalName, System.timeStampInSeconds()]); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter le lieu.` : `Unable to add location.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du lieu.` : `Error adding location.`); + } + return locationId; + } + + /** + * Inserts a new element. + */ + public static insertElement(elementId: string, locationId: string, userId: string, encryptedName: string, originalName: string, description: string | null, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_location_elements (element_id, location_id, user_id, element_name, original_name, element_description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)'; + insertResult = db.run(query, [elementId, locationId, userId, encryptedName, originalName, description, System.timeStampInSeconds()]); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter l'élément.` : `Unable to add element.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout de l'élément.` : `Error adding element.`); + } + return elementId; + } + + /** + * Inserts a new sub-element. + */ + public static insertSubElement(subElementId: string, elementId: string, userId: string, encryptedName: string, originalName: string, description: string | null, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_location_sub_elements (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)'; + insertResult = db.run(query, [subElementId, elementId, userId, encryptedName, originalName, description, System.timeStampInSeconds()]); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter le sous-élément.` : `Unable to add sub-element.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du sous-élément.` : `Error adding sub-element.`); + } + return subElementId; + } + + /** + * Deletes a location section. + */ + public static deleteLocation(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'DELETE FROM series_locations WHERE loc_id = ? AND user_id = ?'; + const deleteResult: RunResult = db.run(query, [locationId, userId]); + return deleteResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de supprimer le lieu.` : `Unable to delete location.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Deletes an element. + */ + public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'DELETE FROM series_location_elements WHERE element_id = ? AND user_id = ?'; + const deleteResult: RunResult = db.run(query, [elementId, userId]); + return deleteResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de supprimer l'élément.` : `Unable to delete element.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Deletes a sub-element. + */ + public static deleteSubElement(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'DELETE FROM series_location_sub_elements WHERE sub_element_id = ? AND user_id = ?'; + const deleteResult: RunResult = db.run(query, [subElementId, userId]); + return deleteResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de supprimer le sous-élément.` : `Unable to delete sub-element.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates a location's name. + */ + public static updateLocation(userId: string, locationId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_locations SET loc_name = ?, loc_original_name = ?, last_update = ? WHERE loc_id = ? AND user_id = ?'; + const updateResult: RunResult = db.run(query, [encryptedName, originalName, System.timeStampInSeconds(), locationId, userId]); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le lieu.` : `Unable to update location.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all locations for a series for sync. + */ + public static fetchSeriesLocationsTable(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationsTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT loc_id, series_id, user_id, loc_name, loc_original_name, last_update FROM series_locations WHERE series_id = ? AND user_id = ?'; + const locations: SeriesLocationsTableResult[] = db.all(query, [seriesId, userId]) as SeriesLocationsTableResult[]; + return locations; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les lieux pour sync.` : `Unable to retrieve locations for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all elements for a location for sync. + */ + public static fetchSeriesLocationElementsTable(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationElementsTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT element_id, location_id, user_id, element_name, original_name, element_description, last_update FROM series_location_elements WHERE location_id = ? AND user_id = ?'; + const elements: SeriesLocationElementsTableResult[] = db.all(query, [locationId, userId]) as SeriesLocationElementsTableResult[]; + return elements; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de lieu pour sync.` : `Unable to retrieve location elements for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all sub-elements for an element for sync. + */ + public static fetchSeriesLocationSubElementsTable(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationSubElementsTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update FROM series_location_sub_elements WHERE element_id = ? AND user_id = ?'; + const subElements: SeriesLocationSubElementsTableResult[] = db.all(query, [elementId, userId]) as SeriesLocationSubElementsTableResult[]; + return subElements; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments pour sync.` : `Unable to retrieve sub-elements for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all series locations for a user for sync comparison. + */ + public static fetchSyncedSeriesLocations(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesLocationResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT loc_id, series_id, loc_name, last_update FROM series_locations WHERE user_id = ?'; + const locations: SyncedSeriesLocationResult[] = db.all(query, [userId]) as SyncedSeriesLocationResult[]; + return locations; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les lieux de série pour sync.` : `Unable to retrieve series locations for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all series location elements for a user for sync comparison. + */ + public static fetchSyncedSeriesLocationElements(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesLocationElementResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT element_id, location_id, element_name, last_update FROM series_location_elements WHERE user_id = ?'; + const elements: SyncedSeriesLocationElementResult[] = db.all(query, [userId]) as SyncedSeriesLocationElementResult[]; + return elements; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de lieu pour sync.` : `Unable to retrieve location elements for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all series location sub-elements for a user for sync comparison. + */ + public static fetchSyncedSeriesLocationSubElements(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesLocationSubElementResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT sub_element_id, element_id, sub_elem_name, last_update FROM series_location_sub_elements WHERE user_id = ?'; + const subElements: SyncedSeriesLocationSubElementResult[] = db.all(query, [userId]) as SyncedSeriesLocationSubElementResult[]; + return subElements; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments de lieu pour sync.` : `Unable to retrieve location sub-elements for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a complete location by ID for sync. + */ + public static fetchCompleteLocationById(locationId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationsTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT loc_id, series_id, user_id, loc_name, loc_original_name, last_update FROM series_locations WHERE loc_id = ?'; + const locations: SeriesLocationsTableResult[] = db.all(query, [locationId]) as SeriesLocationsTableResult[]; + return locations; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le lieu complet.` : `Unable to retrieve complete location.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a complete location element by ID for sync. + */ + public static fetchCompleteLocationElementById(elementId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationElementsTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT element_id, location_id, user_id, element_name, original_name, element_description, last_update FROM series_location_elements WHERE element_id = ?'; + const elements: SeriesLocationElementsTableResult[] = db.all(query, [elementId]) as SeriesLocationElementsTableResult[]; + return elements; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer l'élément de lieu complet.` : `Unable to retrieve complete location element.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a complete location sub-element by ID for sync. + */ + public static fetchCompleteLocationSubElementById(subElementId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationSubElementsTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update FROM series_location_sub_elements WHERE sub_element_id = ?'; + const subElements: SeriesLocationSubElementsTableResult[] = db.all(query, [subElementId]) as SeriesLocationSubElementsTableResult[]; + return subElements; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le sous-élément complet.` : `Unable to retrieve complete sub-element.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Checks if a location exists. + */ + public static isLocationExist(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT 1 FROM series_locations WHERE loc_id = ? AND user_id = ?'; + const result: QueryResult | null = db.get(query, [locationId, userId]); + return result !== null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du lieu.` : `Unable to check location existence.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Checks if a location element exists. + */ + public static isLocationElementExist(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT 1 FROM series_location_elements WHERE element_id = ? AND user_id = ?'; + const result: QueryResult | null = db.get(query, [elementId, userId]); + return result !== null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'élément.` : `Unable to check element existence.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Checks if a location sub-element exists. + */ + public static isLocationSubElementExist(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT 1 FROM series_location_sub_elements WHERE sub_element_id = ? AND user_id = ?'; + const result: QueryResult | null = db.get(query, [subElementId, userId]); + return result !== null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du sous-élément.` : `Unable to check sub-element existence.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a series location for sync. + */ + public static insertSyncLocation(locationId: string, seriesId: string, userId: string, locName: string, locOriginalName: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_locations (loc_id, series_id, user_id, loc_name, loc_original_name, last_update) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(loc_id) DO UPDATE SET loc_name = excluded.loc_name, loc_original_name = excluded.loc_original_name, last_update = excluded.last_update'; + const params: SQLiteValue[] = [locationId, seriesId, userId, locName, locOriginalName, lastUpdate]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le lieu pour sync.` : `Unable to insert location for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates a series location for sync. + */ + public static updateSyncLocation(userId: string, locationId: string, locName: string, locOriginalName: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_locations SET loc_name = ?, loc_original_name = ?, last_update = ? WHERE loc_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [locName, locOriginalName, lastUpdate, locationId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le lieu pour sync.` : `Unable to update location for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a series location element for sync. + */ + public static insertSyncLocationElement(elementId: string, locationId: string, userId: string, elementName: string, originalName: string, elementDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_location_elements (element_id, location_id, user_id, element_name, original_name, element_description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(element_id) DO UPDATE SET element_name = excluded.element_name, original_name = excluded.original_name, element_description = excluded.element_description, last_update = excluded.last_update'; + const params: SQLiteValue[] = [elementId, locationId, userId, elementName, originalName, elementDescription, lastUpdate]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer l'élément de lieu pour sync.` : `Unable to insert location element for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates a series location element for sync. + */ + public static updateSyncLocationElement(userId: string, elementId: string, elementName: string, originalName: string, elementDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_location_elements SET element_name = ?, original_name = ?, element_description = ?, last_update = ? WHERE element_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [elementName, originalName, elementDescription, lastUpdate, elementId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'élément de lieu pour sync.` : `Unable to update location element for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a series location sub-element for sync. + */ + public static insertSyncLocationSubElement(subElementId: string, elementId: string, userId: string, subElemName: string, originalName: string, subElemDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_location_sub_elements (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(sub_element_id) DO UPDATE SET sub_elem_name = excluded.sub_elem_name, original_name = excluded.original_name, sub_elem_description = excluded.sub_elem_description, last_update = excluded.last_update'; + const params: SQLiteValue[] = [subElementId, elementId, userId, subElemName, originalName, subElemDescription, lastUpdate]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le sous-élément pour sync.` : `Unable to insert sub-element for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates a series location sub-element for sync. + */ + public static updateSyncLocationSubElement(userId: string, subElementId: string, subElemName: string, originalName: string, subElemDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_location_sub_elements SET sub_elem_name = ?, original_name = ?, sub_elem_description = ?, last_update = ? WHERE sub_element_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [subElemName, originalName, subElemDescription, lastUpdate, subElementId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sous-élément pour sync.` : `Unable to update sub-element for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } +} diff --git a/electron/database/repositories/series-spell.repo.ts b/electron/database/repositories/series-spell.repo.ts new file mode 100644 index 0000000..75e7896 --- /dev/null +++ b/electron/database/repositories/series-spell.repo.ts @@ -0,0 +1,499 @@ +import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; +import System from "../System.js"; + +export interface SeriesSpellResult extends Record { + spell_id: string; + series_id: string; + name: string; + description: string; + appearance: string; + tags: string; + power_level: string | null; + components: string | null; + limitations: string | null; + notes: string | null; +} + +export interface SeriesSpellTagResult extends Record { + tag_id: string; + name: string; + color: string | null; +} + +export interface SeriesSpellsTableResult extends Record { + spell_id: string; + series_id: string; + user_id: string; + name: string; + name_hash: string; + description: string; + appearance: string; + tags: string; + power_level: string | null; + components: string | null; + limitations: string | null; + notes: string | null; + last_update: number; +} + +export interface SeriesSpellTagsTableResult extends Record { + tag_id: string; + series_id: string; + user_id: string; + name: string; + hashed_name: string; + color: string | null; + last_update: number; +} + +export interface SyncedSeriesSpellResult extends Record { + spell_id: string; + series_id: string; + name: string; + last_update: number; +} + +export interface SyncedSeriesSpellTagResult extends Record { + tag_id: string; + series_id: string; + name: string; + last_update: number; +} + +export default class SeriesSpellRepo { + /** + * Fetches all spells for a specific series. + */ + static fetchSpells(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT spell_id, series_id, name, description, appearance, tags, power_level, components, limitations, notes FROM series_spells WHERE user_id=? AND series_id=?'; + const spells: SeriesSpellResult[] = db.all(query, [userId, seriesId]) as SeriesSpellResult[]; + return spells; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts.` : `Unable to retrieve spells.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a single spell by its ID. + */ + static fetchSpellById(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellResult | null { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT spell_id, series_id, name, description, appearance, tags, power_level, components, limitations, notes FROM series_spells WHERE user_id=? AND spell_id=?'; + const spell: SeriesSpellResult | undefined = db.get(query, [userId, spellId]) as SeriesSpellResult | undefined; + return spell || null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le sort.` : `Unable to retrieve spell.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a new spell. + */ + static insertSpell(spellId: string, seriesId: string, userId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_spells (spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + const params: SQLiteValue[] = [spellId, seriesId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds()]; + insertResult = db.run(query, params); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter le sort.` : `Unable to add spell.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du sort.` : `Error adding spell.`); + } + return spellId; + } + + /** + * Updates an existing spell. + */ + static updateSpell(userId: string, spellId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_spells SET name=?, name_hash=?, description=?, appearance=?, tags=?, power_level=?, components=?, limitations=?, notes=?, last_update=? WHERE spell_id=? AND user_id=?'; + const params: SQLiteValue[] = [name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds(), spellId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort.` : `Unable to update spell.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Deletes a spell. + */ + static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'DELETE FROM series_spells WHERE spell_id=? AND user_id=?'; + const deleteResult: RunResult = db.run(query, [spellId, userId]); + return deleteResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de supprimer le sort.` : `Unable to delete spell.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all spell tags for a series. + */ + static fetchTags(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellTagResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT tag_id, name, color FROM series_spell_tags WHERE user_id=? AND series_id=?'; + const tags: SeriesSpellTagResult[] = db.all(query, [userId, seriesId]) as SeriesSpellTagResult[]; + return tags; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les tags.` : `Unable to retrieve tags.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a new spell tag. + */ + static insertTag(tagId: string, seriesId: string, userId: string, name: string, hashedName: string, color: string | null, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_spell_tags (tag_id, series_id, user_id, name, hashed_name, color, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)'; + const params: SQLiteValue[] = [tagId, seriesId, userId, name, hashedName, color, System.timeStampInSeconds()]; + insertResult = db.run(query, params); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter le tag.` : `Unable to add tag.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du tag.` : `Error adding tag.`); + } + return tagId; + } + + /** + * Updates an existing spell tag. + */ + static updateTag(userId: string, tagId: string, name: string, hashedName: string, color: string | null, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_spell_tags SET name=?, hashed_name=?, color=?, last_update=? WHERE tag_id=? AND user_id=?'; + const params: SQLiteValue[] = [name, hashedName, color, System.timeStampInSeconds(), tagId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le tag.` : `Unable to update tag.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Deletes a spell tag. + */ + static deleteTag(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'DELETE FROM series_spell_tags WHERE tag_id=? AND user_id=?'; + const deleteResult: RunResult = db.run(query, [tagId, userId]); + return deleteResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de supprimer le tag.` : `Unable to delete tag.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Checks if a spell exists. + */ + static isSpellExist(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT 1 FROM series_spells WHERE spell_id=? AND user_id=?'; + const result: QueryResult | null = db.get(query, [spellId, userId]); + return result !== null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du sort.` : `Unable to check spell existence.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all spells for a series for sync. + */ + static fetchSeriesSpellsTable(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellsTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE series_id = ? AND user_id = ?'; + const spells: SeriesSpellsTableResult[] = db.all(query, [seriesId, userId]) as SeriesSpellsTableResult[]; + return spells; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts pour sync.` : `Unable to retrieve spells for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all spell tags for a series for sync. + */ + static fetchSeriesSpellTagsTable(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellTagsTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE series_id = ? AND user_id = ?'; + const tags: SeriesSpellTagsTableResult[] = db.all(query, [seriesId, userId]) as SeriesSpellTagsTableResult[]; + return tags; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sort pour sync.` : `Unable to retrieve spell tags for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all series spells for a user for sync comparison. + */ + static fetchSyncedSeriesSpells(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesSpellResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT spell_id, series_id, name, last_update FROM series_spells WHERE user_id = ?'; + const spells: SyncedSeriesSpellResult[] = db.all(query, [userId]) as SyncedSeriesSpellResult[]; + return spells; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts de série pour sync.` : `Unable to retrieve series spells for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all series spell tags for a user for sync comparison. + */ + static fetchSyncedSeriesSpellTags(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesSpellTagResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT tag_id, series_id, name, last_update FROM series_spell_tags WHERE user_id = ?'; + const tags: SyncedSeriesSpellTagResult[] = db.all(query, [userId]) as SyncedSeriesSpellTagResult[]; + return tags; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sort pour sync.` : `Unable to retrieve spell tags for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a complete spell by ID for sync. + */ + static fetchSpellTableById(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellsTableResult | null { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE spell_id = ? AND user_id = ?'; + const spell: SeriesSpellsTableResult | undefined = db.get(query, [spellId, userId]) as SeriesSpellsTableResult | undefined; + return spell || null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le sort complet.` : `Unable to retrieve complete spell.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a complete spell tag by ID for sync. + */ + static fetchSpellTagTableById(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellTagsTableResult | null { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE tag_id = ? AND user_id = ?'; + const tag: SeriesSpellTagsTableResult | undefined = db.get(query, [tagId, userId]) as SeriesSpellTagsTableResult | undefined; + return tag || null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le tag complet.` : `Unable to retrieve complete tag.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Checks if a spell tag exists. + */ + static isSpellTagExist(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT 1 FROM series_spell_tags WHERE tag_id=? AND user_id=?'; + const result: QueryResult | null = db.get(query, [tagId, userId]); + return result !== null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du tag.` : `Unable to check tag existence.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a series spell for sync. + */ + static insertSyncSpell(spellId: string, seriesId: string, userId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_spells (spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(spell_id) DO UPDATE SET name = excluded.name, name_hash = excluded.name_hash, description = excluded.description, appearance = excluded.appearance, tags = excluded.tags, power_level = excluded.power_level, components = excluded.components, limitations = excluded.limitations, notes = excluded.notes, last_update = excluded.last_update'; + const params: SQLiteValue[] = [spellId, seriesId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le sort pour sync.` : `Unable to insert spell for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates a series spell for sync. + */ + static updateSyncSpell(userId: string, spellId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_spells SET name = ?, name_hash = ?, description = ?, appearance = ?, tags = ?, power_level = ?, components = ?, limitations = ?, notes = ?, last_update = ? WHERE spell_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate, spellId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort pour sync.` : `Unable to update spell for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a series spell tag for sync. + */ + static insertSyncSpellTag(tagId: string, seriesId: string, userId: string, name: string, hashedName: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_spell_tags (tag_id, series_id, user_id, name, hashed_name, color, last_update) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(tag_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, color = excluded.color, last_update = excluded.last_update'; + const params: SQLiteValue[] = [tagId, seriesId, userId, name, hashedName, color, lastUpdate]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le tag pour sync.` : `Unable to insert tag for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates a series spell tag for sync. + */ + static updateSyncSpellTag(userId: string, tagId: string, name: string, hashedName: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_spell_tags SET name = ?, hashed_name = ?, color = ?, last_update = ? WHERE tag_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [name, hashedName, color, lastUpdate, tagId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le tag pour sync.` : `Unable to update tag for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } +} diff --git a/electron/database/repositories/series-sync.repo.ts b/electron/database/repositories/series-sync.repo.ts new file mode 100644 index 0000000..fb401b6 --- /dev/null +++ b/electron/database/repositories/series-sync.repo.ts @@ -0,0 +1,258 @@ +import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; +import System from "../System.js"; + +export type SyncElementType = 'character' | 'world' | 'location' | 'spell'; + +export interface BookElementSeriesLink extends Record { + series_id: string | null; +} + +export default class SeriesSyncRepo { + /** + * Gets the series element ID linked to a book character. + */ + static getCharacterSeriesLink(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): string | null { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT series_character_id AS series_id FROM book_characters WHERE character_id = ? AND user_id = ?'; + const result: BookElementSeriesLink | undefined = db.get(query, [characterId, userId]) as BookElementSeriesLink | undefined; + return result ? result.series_id : null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le lien série du personnage.` : `Unable to retrieve character series link.`); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + /** + * Gets the series element ID linked to a book world. + */ + static getWorldSeriesLink(userId: string, worldId: string, lang: 'fr' | 'en' = 'fr'): string | null { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT series_world_id AS series_id FROM book_world WHERE world_id = ? AND user_id = ?'; + const result: BookElementSeriesLink | undefined = db.get(query, [worldId, userId]) as BookElementSeriesLink | undefined; + return result ? result.series_id : null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le lien série du monde.` : `Unable to retrieve world series link.`); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + /** + * Gets the series element ID linked to a book location. + */ + static getLocationSeriesLink(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): string | null { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT series_location_id AS series_id FROM book_location WHERE loc_id = ? AND user_id = ?'; + const result: BookElementSeriesLink | undefined = db.get(query, [locationId, userId]) as BookElementSeriesLink | undefined; + return result ? result.series_id : null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le lien série du lieu.` : `Unable to retrieve location series link.`); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + /** + * Gets the series element ID linked to a book spell. + */ + static getSpellSeriesLink(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): string | null { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT series_spell_id AS series_id FROM book_spells WHERE spell_id = ? AND user_id = ?'; + const result: BookElementSeriesLink | undefined = db.get(query, [spellId, userId]) as BookElementSeriesLink | undefined; + return result ? result.series_id : null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le lien série du sort.` : `Unable to retrieve spell series link.`); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + /** + * Updates a field in series_characters table. + */ + static updateSeriesCharacterField(userId: string, seriesCharacterId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): boolean { + const allowedFields: string[] = ['first_name', 'last_name', 'nickname', 'age', 'gender', 'species', 'nationality', 'status', 'title', 'category', 'role', 'biography', 'history', 'speech_pattern', 'catchphrase', 'residence', 'notes', 'color']; + if (!allowedFields.includes(field)) { + throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); + } + try { + const db: Database = System.getDb(); + const query: string = `UPDATE series_characters SET ${field} = ?, last_update = ? WHERE character_id = ? AND user_id = ?`; + const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesCharacterId, userId]); + return result.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le personnage série.` : `Unable to update series character.`); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + /** + * Updates a field in all book_characters linked to a series character. + */ + static updateLinkedBookCharactersField(userId: string, seriesCharacterId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): number { + const allowedFields: string[] = ['first_name', 'last_name', 'nickname', 'age', 'gender', 'species', 'nationality', 'status', 'title', 'category', 'role', 'biography', 'history', 'speech_pattern', 'catchphrase', 'residence', 'notes', 'color']; + if (!allowedFields.includes(field)) { + throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); + } + try { + const db: Database = System.getDb(); + const query: string = `UPDATE book_characters SET ${field} = ?, last_update = ? WHERE series_character_id = ? AND user_id = ?`; + const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesCharacterId, userId]); + return result.changes; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour les personnages liés.` : `Unable to update linked characters.`); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + /** + * Updates a field in series_worlds table. + */ + static updateSeriesWorldField(userId: string, seriesWorldId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): boolean { + const allowedFields: string[] = ['name', 'history', 'politics', 'economy', 'religion', 'languages']; + if (!allowedFields.includes(field)) { + throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); + } + try { + const db: Database = System.getDb(); + const query: string = `UPDATE series_worlds SET ${field} = ?, last_update = ? WHERE world_id = ? AND user_id = ?`; + const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesWorldId, userId]); + return result.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le monde série.` : `Unable to update series world.`); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + /** + * Updates a field in all book_world linked to a series world. + */ + static updateLinkedBookWorldsField(userId: string, seriesWorldId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): number { + const allowedFields: string[] = ['name', 'history', 'politics', 'economy', 'religion', 'languages']; + if (!allowedFields.includes(field)) { + throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); + } + try { + const db: Database = System.getDb(); + const query: string = `UPDATE book_world SET ${field} = ?, last_update = ? WHERE series_world_id = ? AND user_id = ?`; + const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesWorldId, userId]); + return result.changes; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour les mondes liés.` : `Unable to update linked worlds.`); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + /** + * Updates a field in series_locations table. + */ + static updateSeriesLocationField(userId: string, seriesLocationId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): boolean { + const allowedFields: string[] = ['name']; + if (!allowedFields.includes(field)) { + throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); + } + try { + const db: Database = System.getDb(); + const query: string = `UPDATE series_locations SET ${field} = ?, last_update = ? WHERE location_id = ? AND user_id = ?`; + const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesLocationId, userId]); + return result.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le lieu série.` : `Unable to update series location.`); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + /** + * Updates a field in all book_location linked to a series location. + */ + static updateLinkedBookLocationsField(userId: string, seriesLocationId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): number { + const allowedFields: string[] = ['loc_name']; + if (!allowedFields.includes(field)) { + throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); + } + try { + const db: Database = System.getDb(); + const query: string = `UPDATE book_location SET ${field} = ?, last_update = ? WHERE series_location_id = ? AND user_id = ?`; + const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesLocationId, userId]); + return result.changes; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour les lieux liés.` : `Unable to update linked locations.`); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + /** + * Updates a field in series_spells table. + */ + static updateSeriesSpellField(userId: string, seriesSpellId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): boolean { + const allowedFields: string[] = ['name', 'description', 'type', 'level', 'range', 'duration', 'cost', 'effect', 'components', 'notes']; + if (!allowedFields.includes(field)) { + throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); + } + try { + const db: Database = System.getDb(); + const query: string = `UPDATE series_spells SET ${field} = ?, last_update = ? WHERE spell_id = ? AND user_id = ?`; + const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesSpellId, userId]); + return result.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort série.` : `Unable to update series spell.`); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + + /** + * Updates a field in all book_spells linked to a series spell. + */ + static updateLinkedBookSpellsField(userId: string, seriesSpellId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): number { + const allowedFields: string[] = ['name', 'description', 'type', 'level', 'range', 'duration', 'cost', 'effect', 'components', 'notes']; + if (!allowedFields.includes(field)) { + throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); + } + try { + const db: Database = System.getDb(); + const query: string = `UPDATE book_spells SET ${field} = ?, last_update = ? WHERE series_spell_id = ? AND user_id = ?`; + const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesSpellId, userId]); + return result.changes; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour les sorts liés.` : `Unable to update linked spells.`); + } + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } +} diff --git a/electron/database/repositories/series-world.repo.ts b/electron/database/repositories/series-world.repo.ts new file mode 100644 index 0000000..457ca0b --- /dev/null +++ b/electron/database/repositories/series-world.repo.ts @@ -0,0 +1,432 @@ +import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; +import System from "../System.js"; + +export interface SeriesWorldResult extends Record { + world_id: string; + world_name: string; + history: string; + politics: string; + economy: string; + religion: string; + languages: string; + element_id: string; + element_name: string; + element_description: string; + element_type: number; +} + +export interface SeriesWorldsTableResult extends Record { + world_id: string; + series_id: string; + user_id: string; + name: string; + hashed_name: string; + history: string | null; + politics: string | null; + economy: string | null; + religion: string | null; + languages: string | null; + last_update: number; +} + +export interface SeriesWorldElementsTableResult extends Record { + element_id: string; + world_id: string; + user_id: string; + element_type: number; + name: string; + original_name: string; + description: string | null; + last_update: number; +} + +export interface SyncedSeriesWorldResult extends Record { + world_id: string; + series_id: string; + name: string; + last_update: number; +} + +export interface SyncedSeriesWorldElementResult extends Record { + element_id: string; + world_id: string; + name: string; + last_update: number; +} + +export default class SeriesWorldRepo { + /** + * Checks if a world with the given hashed name already exists for a user and series. + */ + public static checkWorldExist(userId: string, seriesId: string, worldName: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT world_id FROM series_worlds WHERE user_id=? AND series_id=? AND hashed_name=?'; + const result: QueryResult | null = db.get(query, [userId, seriesId, worldName]); + return result !== null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du monde.` : `Unable to verify world existence.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a new world into the series. + */ + public static insertNewWorld(worldId: string, userId: string, seriesId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_worlds (world_id, user_id, series_id, name, hashed_name, last_update) VALUES (?,?,?,?,?,?)'; + const params: SQLiteValue[] = [worldId, userId, seriesId, encryptedName, hashedName, System.timeStampInSeconds()]; + insertResult = db.run(query, params); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter le monde.` : `Unable to add world.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du monde.` : `Error adding world.`); + } + return worldId; + } + + /** + * Fetches all worlds and their elements for a given series. + */ + public static fetchWorlds(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT world.world_id AS world_id, world.name AS world_name, world.history, world.politics, world.economy, world.religion, world.languages, element.element_id AS element_id, element.name AS element_name, element.description AS element_description, element.element_type FROM series_worlds AS world LEFT JOIN series_world_elements AS element ON world.world_id = element.world_id WHERE world.user_id = ? AND world.series_id = ?'; + const worlds: SeriesWorldResult[] = db.all(query, [userId, seriesId]) as SeriesWorldResult[]; + return worlds; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les mondes.` : `Unable to retrieve worlds.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates a world's information. + */ + public static updateWorld(userId: string, worldId: string, encryptedName: string, hashedName: string, history: string | null, politics: string | null, economy: string | null, religion: string | null, languages: string | null, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_worlds SET name=?, hashed_name=?, history=?, politics=?, economy=?, religion=?, languages=?, last_update=? WHERE world_id=? AND user_id=?'; + const params: SQLiteValue[] = [encryptedName, hashedName, history, politics, economy, religion, languages, System.timeStampInSeconds(), worldId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le monde.` : `Unable to update world.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a new element for a world. + */ + public static insertElement(elementId: string, worldId: string, userId: string, elementType: number, encryptedName: string, originalName: string, description: string | null, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?,?,?,?,?,?,?,?)'; + const params: SQLiteValue[] = [elementId, worldId, userId, elementType, encryptedName, originalName, description, System.timeStampInSeconds()]; + insertResult = db.run(query, params); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter l'élément.` : `Unable to add element.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout de l'élément.` : `Error adding element.`); + } + return elementId; + } + + /** + * Deletes an element from a world. + */ + public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'DELETE FROM series_world_elements WHERE element_id=? AND user_id=?'; + const deleteResult: RunResult = db.run(query, [elementId, userId]); + return deleteResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de supprimer l'élément.` : `Unable to delete element.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all worlds for a series for sync. + */ + public static fetchSeriesWorldsTable(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldsTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE series_id = ? AND user_id = ?'; + const worlds: SeriesWorldsTableResult[] = db.all(query, [seriesId, userId]) as SeriesWorldsTableResult[]; + return worlds; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les mondes pour sync.` : `Unable to retrieve worlds for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all elements for a world for sync. + */ + public static fetchSeriesWorldElementsTable(userId: string, worldId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldElementsTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM series_world_elements WHERE world_id = ? AND user_id = ?'; + const elements: SeriesWorldElementsTableResult[] = db.all(query, [worldId, userId]) as SeriesWorldElementsTableResult[]; + return elements; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de monde pour sync.` : `Unable to retrieve world elements for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all series worlds for a user for sync comparison. + */ + public static fetchSyncedSeriesWorlds(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesWorldResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT world_id, series_id, name, last_update FROM series_worlds WHERE user_id = ?'; + const worlds: SyncedSeriesWorldResult[] = db.all(query, [userId]) as SyncedSeriesWorldResult[]; + return worlds; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les mondes de série pour sync.` : `Unable to retrieve series worlds for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all series world elements for a user for sync comparison. + */ + public static fetchSyncedSeriesWorldElements(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesWorldElementResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT element_id, world_id, name, last_update FROM series_world_elements WHERE user_id = ?'; + const elements: SyncedSeriesWorldElementResult[] = db.all(query, [userId]) as SyncedSeriesWorldElementResult[]; + return elements; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de monde pour sync.` : `Unable to retrieve world elements for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a complete world by ID for sync. + */ + public static fetchCompleteWorldById(worldId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldsTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE world_id = ?'; + const worlds: SeriesWorldsTableResult[] = db.all(query, [worldId]) as SeriesWorldsTableResult[]; + return worlds; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le monde complet.` : `Unable to retrieve complete world.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a complete world element by ID for sync. + */ + public static fetchCompleteWorldElementById(elementId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldElementsTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM series_world_elements WHERE element_id = ?'; + const elements: SeriesWorldElementsTableResult[] = db.all(query, [elementId]) as SeriesWorldElementsTableResult[]; + return elements; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer l'élément de monde complet.` : `Unable to retrieve complete world element.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Checks if a world exists. + */ + public static isWorldExist(userId: string, worldId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT 1 FROM series_worlds WHERE world_id=? AND user_id=?'; + const result: QueryResult | null = db.get(query, [worldId, userId]); + return result !== null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du monde.` : `Unable to check world existence.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Checks if a world element exists. + */ + public static isWorldElementExist(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT 1 FROM series_world_elements WHERE element_id=? AND user_id=?'; + const result: QueryResult | null = db.get(query, [elementId, userId]); + return result !== null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'élément.` : `Unable to check element existence.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a series world for sync. + */ + public static insertSyncWorld(worldId: string, seriesId: string, userId: string, name: string, hashedName: string, history: string | null, politics: string | null, economy: string | null, religion: string | null, languages: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_worlds (world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(world_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, history = excluded.history, politics = excluded.politics, economy = excluded.economy, religion = excluded.religion, languages = excluded.languages, last_update = excluded.last_update'; + const params: SQLiteValue[] = [worldId, seriesId, userId, name, hashedName, history, politics, economy, religion, languages, lastUpdate]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer le monde pour sync.` : `Unable to insert world for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates a series world for sync. + */ + public static updateSyncWorld(userId: string, worldId: string, name: string, hashedName: string, history: string | null, politics: string | null, economy: string | null, religion: string | null, languages: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_worlds SET name = ?, hashed_name = ?, history = ?, politics = ?, economy = ?, religion = ?, languages = ?, last_update = ? WHERE world_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [name, hashedName, history, politics, economy, religion, languages, lastUpdate, worldId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le monde pour sync.` : `Unable to update world for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a series world element for sync. + */ + public static insertSyncWorldElement(elementId: string, worldId: string, userId: string, elementType: number, name: string, originalName: string, description: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(element_id) DO UPDATE SET element_type = excluded.element_type, name = excluded.name, original_name = excluded.original_name, description = excluded.description, last_update = excluded.last_update'; + const params: SQLiteValue[] = [elementId, worldId, userId, elementType, name, originalName, description, lastUpdate]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer l'élément de monde pour sync.` : `Unable to insert world element for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates a series world element for sync. + */ + public static updateSyncWorldElement(userId: string, elementId: string, elementType: number, name: string, originalName: string, description: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE series_world_elements SET element_type = ?, name = ?, original_name = ?, description = ?, last_update = ? WHERE element_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [elementType, name, originalName, description, lastUpdate, elementId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'élément de monde pour sync.` : `Unable to update world element for sync.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } +} diff --git a/electron/database/repositories/series.repo.ts b/electron/database/repositories/series.repo.ts new file mode 100644 index 0000000..6402ddd --- /dev/null +++ b/electron/database/repositories/series.repo.ts @@ -0,0 +1,542 @@ +import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; +import System from "../System.js"; + +export interface SeriesResult extends Record { + series_id: string; + user_id: string; + name: string; + hashed_name: string; + description: string | null; + cover_image: string | null; + last_update: number; +} + +export interface SeriesBookResult extends Record { + series_id: string; + book_id: string; + book_order: number; + title: string; + cover_image: string | null; +} + +export interface SeriesListItem extends Record { + series_id: string; + name: string; + description: string | null; + cover_image: string | null; + book_count: number; + book_ids: string | null; +} + +export interface SeriesTableResult extends Record { + series_id: string; + user_id: string; + name: string; + hashed_name: string; + description: string | null; + cover_image: string | null; + last_update: number; +} + +export interface SeriesBooksTableResult extends Record { + series_id: string; + book_id: string; + book_order: number; + last_update: number; +} + +export interface SyncedSeriesResult extends Record { + series_id: string; + name: string; + description: string | null; + last_update: number; +} + +export interface SyncedSeriesBookResult extends Record { + series_id: string; + book_id: string; + book_order: number; + last_update: number; +} + +export default class SeriesRepo { + /** + * Fetches all series for a user. + * @param userId - The unique identifier of the user + * @param lang - The language for error messages ('fr' or 'en') + * @returns An array of series with book counts + */ + public static fetchUserSeries(userId: string, lang: 'fr' | 'en' = 'fr'): SeriesListItem[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT series.series_id, series.name, series.description, series.cover_image, COUNT(series_books.book_id) AS book_count, GROUP_CONCAT(series_books.book_id) AS book_ids FROM book_series series LEFT JOIN series_books ON series.series_id = series_books.series_id WHERE series.user_id = ? GROUP BY series.series_id, series.last_update ORDER BY series.last_update DESC'; + const series: SeriesListItem[] = db.all(query, [userId]) as SeriesListItem[]; + return series; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les séries.` : `Unable to retrieve series.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a single series by its ID. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns The series result or null if not found + */ + public static fetchSeriesById(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesResult | null { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ? AND user_id = ?'; + const series: SeriesResult | undefined = db.get(query, [seriesId, userId]) as SeriesResult | undefined; + return series || null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer la série.` : `Unable to retrieve series.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a new series. + * @param seriesId - The unique identifier for the new series + * @param userId - The unique identifier of the user + * @param name - The encrypted name + * @param hashedName - The hashed name for duplicate detection + * @param description - The encrypted description (nullable) + * @param lang - The language for error messages ('fr' or 'en') + * @returns The series ID if successful + */ + public static insertSeries(seriesId: string, userId: string, name: string, hashedName: string, description: string | null, lang: 'fr' | 'en' = 'fr'): string { + let insertResult: RunResult; + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO book_series (series_id, user_id, name, hashed_name, description, last_update) VALUES (?, ?, ?, ?, ?, ?)'; + const params: SQLiteValue[] = [seriesId, userId, name, hashedName, description, System.timeStampInSeconds()]; + insertResult = db.run(query, params); + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de créer la série.` : `Unable to create series.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (!insertResult || insertResult.changes === 0) { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de la création de la série.` : `Error creating series.`); + } + return seriesId; + } + + /** + * Updates an existing series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param name - The encrypted name + * @param hashedName - The hashed name + * @param description - The encrypted description (nullable) + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the update was successful + */ + public static updateSeries(userId: string, seriesId: string, name: string, hashedName: string, description: string | null, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE book_series SET name = ?, hashed_name = ?, description = ?, last_update = ? WHERE series_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [name, hashedName, description, System.timeStampInSeconds(), seriesId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour la série.` : `Unable to update series.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Deletes a series. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the deletion was successful + */ + public static deleteSeries(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'DELETE FROM book_series WHERE series_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [seriesId, userId]; + const deleteResult: RunResult = db.run(query, params); + return deleteResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de supprimer la série.` : `Unable to delete series.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all books in a series with their order. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns An array of books in the series + */ + public static fetchSeriesBooks(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesBookResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT sb.series_id, sb.book_id, sb.book_order, b.title, b.cover_image FROM series_books sb INNER JOIN erit_books b ON sb.book_id = b.book_id WHERE sb.series_id = ? AND b.author_id = ? ORDER BY sb.book_order'; + const books: SeriesBookResult[] = db.all(query, [seriesId, userId]) as SeriesBookResult[]; + return books; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les livres de la série.` : `Unable to retrieve series books.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Adds a book to a series. + * @param seriesId - The unique identifier of the series + * @param bookId - The unique identifier of the book + * @param bookOrder - The order of the book in the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the addition was successful + */ + public static addBookToSeries(seriesId: string, bookId: string, bookOrder: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_books (series_id, book_id, book_order, last_update) VALUES (?, ?, ?, ?) ON CONFLICT(series_id, book_id) DO UPDATE SET book_order = excluded.book_order, last_update = excluded.last_update'; + const params: SQLiteValue[] = [seriesId, bookId, bookOrder, System.timeStampInSeconds()]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter le livre à la série.` : `Unable to add book to series.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Removes a book from a series. + * @param seriesId - The unique identifier of the series + * @param bookId - The unique identifier of the book + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the removal was successful + */ + public static removeBookFromSeries(seriesId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'DELETE FROM series_books WHERE series_id = ? AND book_id = ?'; + const params: SQLiteValue[] = [seriesId, bookId]; + const deleteResult: RunResult = db.run(query, params); + return deleteResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de retirer le livre de la série.` : `Unable to remove book from series.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates the order of books in a series. + * @param seriesId - The unique identifier of the series + * @param booksOrder - An array of {bookId, order} objects + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the update was successful + */ + public static updateBooksOrder(seriesId: string, booksOrder: {bookId: string, order: number}[], lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const timestamp: number = System.timeStampInSeconds(); + for (const bookOrder of booksOrder) { + const query: string = 'UPDATE series_books SET book_order = ?, last_update = ? WHERE series_id = ? AND book_id = ?'; + db.run(query, [bookOrder.order, timestamp, seriesId, bookOrder.bookId]); + } + return true; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de réordonner les livres.` : `Unable to reorder books.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Checks if a series exists for a user. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the series exists + */ + public static isSeriesExist(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT 1 FROM book_series WHERE series_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [seriesId, userId]; + const result: QueryResult | null = db.get(query, params); + return result !== null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de la série.` : `Unable to check series existence.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Gets the series ID for a book if it belongs to one. + * @param bookId - The unique identifier of the book + * @param lang - The language for error messages ('fr' or 'en') + * @returns The series ID or null + */ + public static getSeriesIdForBook(bookId: string, lang: 'fr' | 'en' = 'fr'): string | null { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT series_id FROM series_books WHERE book_id = ?'; + const result = db.get(query, [bookId]) as { series_id: string } | undefined; + return result ? result.series_id : null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier la série du livre.` : `Unable to check book series.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a series table row for sync purposes. + * @param userId - The unique identifier of the user + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns An array containing the series table row + */ + public static fetchSeriesTableForSync(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ? AND user_id = ?'; + const series: SeriesTableResult[] = db.all(query, [seriesId, userId]) as SeriesTableResult[]; + return series; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer la série pour sync.` : `Unable to retrieve series for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all series-books relationships for sync. + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns An array of series-books table rows + */ + public static fetchSeriesBooksTable(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesBooksTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT series_id, book_id, book_order, last_update FROM series_books WHERE series_id = ? ORDER BY book_order'; + const books: SeriesBooksTableResult[] = db.all(query, [seriesId]) as SeriesBooksTableResult[]; + return books; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les livres de la série pour sync.` : `Unable to retrieve series books for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all series for a user for sync comparison. + * @param userId - The unique identifier of the user + * @param lang - The language for error messages ('fr' or 'en') + * @returns An array of synced series results + */ + public static fetchSyncedSeries(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT series_id, name, description, last_update FROM book_series WHERE user_id = ? ORDER BY last_update DESC'; + const series: SyncedSeriesResult[] = db.all(query, [userId]) as SyncedSeriesResult[]; + return series; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les séries pour sync.` : `Unable to retrieve series for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches all series-books relationships for a user for sync comparison. + * @param userId - The unique identifier of the user + * @param lang - The language for error messages ('fr' or 'en') + * @returns An array of synced series book results + */ + public static fetchSyncedSeriesBooks(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesBookResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT sb.series_id, sb.book_id, sb.book_order, sb.last_update FROM series_books sb INNER JOIN book_series bs ON sb.series_id = bs.series_id WHERE bs.user_id = ? ORDER BY sb.book_order'; + const books: SyncedSeriesBookResult[] = db.all(query, [userId]) as SyncedSeriesBookResult[]; + return books; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les livres de séries pour sync.` : `Unable to retrieve series books for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Fetches a complete series by ID for sync. + * @param seriesId - The unique identifier of the series + * @param lang - The language for error messages ('fr' or 'en') + * @returns An array containing the series + */ + public static fetchCompleteSeriesById(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesTableResult[] { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ?'; + const series: SeriesTableResult[] = db.all(query, [seriesId]) as SeriesTableResult[]; + return series; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer la série complète.` : `Unable to retrieve complete series.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a series for sync purposes. + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the insertion was successful + */ + public static insertSyncSeries(seriesId: string, userId: string, name: string, hashedName: string, description: string | null, coverImage: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO book_series (series_id, user_id, name, hashed_name, description, cover_image, last_update) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(series_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, description = excluded.description, cover_image = excluded.cover_image, last_update = excluded.last_update'; + const params: SQLiteValue[] = [seriesId, userId, name, hashedName, description, coverImage, lastUpdate]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer la série pour sync.` : `Unable to insert series for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Updates a series for sync purposes. + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the update was successful + */ + public static updateSyncSeries(userId: string, seriesId: string, name: string, hashedName: string, description: string | null, coverImage: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'UPDATE book_series SET name = ?, hashed_name = ?, description = ?, cover_image = ?, last_update = ? WHERE series_id = ? AND user_id = ?'; + const params: SQLiteValue[] = [name, hashedName, description, coverImage, lastUpdate, seriesId, userId]; + const updateResult: RunResult = db.run(query, params); + return updateResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour la série pour sync.` : `Unable to update series for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Inserts a series-book relationship for sync. + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the insertion was successful + */ + public static insertSyncSeriesBook(seriesId: string, bookId: string, bookOrder: number, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'INSERT INTO series_books (series_id, book_id, book_order, last_update) VALUES (?, ?, ?, ?) ON CONFLICT(series_id, book_id) DO UPDATE SET book_order = excluded.book_order, last_update = excluded.last_update'; + const params: SQLiteValue[] = [seriesId, bookId, bookOrder, lastUpdate]; + const insertResult: RunResult = db.run(query, params); + return insertResult.changes > 0; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible d'insérer la liaison série-livre pour sync.` : `Unable to insert series-book for sync.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + /** + * Checks if a series-book relationship exists. + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the relationship exists + */ + public static isSeriesBookExist(seriesId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const query: string = 'SELECT 1 FROM series_books WHERE series_id = ? AND book_id = ?'; + const result: QueryResult | null = db.get(query, [seriesId, bookId]); + return result !== null; + } catch (error: unknown) { + if (error instanceof Error) { + console.error(`DB Error: ${error.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier la liaison série-livre.` : `Unable to check series-book.`); + } else { + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } +} diff --git a/electron/database/repositories/spell.repo.ts b/electron/database/repositories/spell.repo.ts index 23f42cb..9975f05 100644 --- a/electron/database/repositories/spell.repo.ts +++ b/electron/database/repositories/spell.repo.ts @@ -12,6 +12,7 @@ export interface SpellResult extends Record { components: string | null; limitations: string | null; notes: string | null; + series_spell_id: string | null; } export interface BookSpellsTable extends Record { @@ -48,7 +49,7 @@ export default class SpellRepo { static fetchSpells(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellResult[] { try { const db: Database = System.getDb(); - const query: string = 'SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes FROM book_spells WHERE user_id=? AND book_id=?'; + const query: string = 'SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes, series_spell_id FROM book_spells WHERE user_id=? AND book_id=?'; const params: SQLiteValue[] = [userId, bookId]; return db.all(query, params) as SpellResult[]; } catch (error: unknown) { @@ -71,7 +72,7 @@ export default class SpellRepo { static fetchSpellById(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SpellResult | null { try { const db: Database = System.getDb(); - const query: string = 'SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes FROM book_spells WHERE user_id=? AND spell_id=?'; + const query: string = 'SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes, series_spell_id FROM book_spells WHERE user_id=? AND spell_id=?'; const params: SQLiteValue[] = [userId, spellId]; const spells: SpellResult[] = db.all(query, params) as SpellResult[]; return spells.length > 0 ? spells[0] : null; @@ -102,16 +103,17 @@ export default class SpellRepo { * @param lang - The language for error messages ('fr' or 'en') * @returns The spell ID if successful */ - static insertSpell(spellId: string, bookId: string, userId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): string { + static insertSpell(spellId: string, bookId: string, userId: string, name: string, nameHash: string, description: string | null, appearance: string | null, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr', seriesSpellId: string | null = null): string { + let result: RunResult; try { const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)'; - const params: SQLiteValue[] = [spellId, bookId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds()]; - const result: RunResult = db.run(query, params); - if (!result || result.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du sort.` : `Error adding spell.`); - } - return spellId; + const query: string = seriesSpellId + ? 'INSERT INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, series_spell_id, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)' + : 'INSERT INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)'; + const params: SQLiteValue[] = seriesSpellId + ? [spellId, bookId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, seriesSpellId, System.timeStampInSeconds()] + : [spellId, bookId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds()]; + result = db.run(query, params); } catch (error: unknown) { if (error instanceof Error) { console.error(`[SpellRepo] DB Error: ${error.message}`); @@ -120,6 +122,10 @@ export default class SpellRepo { } throw new Error(lang === 'fr' ? `Impossible d'ajouter le sort.` : `Unable to add spell.`); } + if (!result || result.changes === 0) { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du sort.` : `Error adding spell.`); + } + return spellId; } /** @@ -138,11 +144,15 @@ export default class SpellRepo { * @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, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): boolean { + static updateSpell(userId: string, spellId: string, name: string, nameHash: string, description: string | null, appearance: string | null, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr', seriesSpellId: string | null = null): boolean { try { const db: Database = System.getDb(); - const query: string = 'UPDATE book_spells SET name=?, name_hash=?, description=?, appearance=?, tags=?, power_level=?, components=?, limitations=?, notes=?, last_update=? WHERE spell_id=? AND user_id=?'; - const params: SQLiteValue[] = [name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds(), spellId, userId]; + const query: string = seriesSpellId !== null + ? 'UPDATE book_spells SET name=?, name_hash=?, description=?, appearance=?, tags=?, power_level=?, components=?, limitations=?, notes=?, series_spell_id=?, last_update=? WHERE spell_id=? AND user_id=?' + : 'UPDATE book_spells SET name=?, name_hash=?, description=?, appearance=?, tags=?, power_level=?, components=?, limitations=?, notes=?, last_update=? WHERE spell_id=? AND user_id=?'; + const params: SQLiteValue[] = seriesSpellId !== null + ? [name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, seriesSpellId, System.timeStampInSeconds(), spellId, userId] + : [name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds(), spellId, userId]; const result: RunResult = db.run(query, params); return result.changes > 0; } catch (error: unknown) { diff --git a/electron/database/repositories/spelltag.repo.ts b/electron/database/repositories/spelltag.repo.ts index b6fe803..da6be39 100644 --- a/electron/database/repositories/spelltag.repo.ts +++ b/electron/database/repositories/spelltag.repo.ts @@ -62,15 +62,12 @@ export default class SpellTagRepo { * @returns The tag ID if successful */ static insertSpellTag(tagId: string, bookId: string, userId: string, name: string, nameHash: string, color: string | null, lang: 'fr' | 'en' = 'fr'): string { + let result: RunResult; try { const db: Database = System.getDb(); const query: string = 'INSERT INTO book_spell_tags (tag_id, book_id, user_id, name, name_hash, color, last_update) VALUES (?,?,?,?,?,?,?)'; const params: SQLiteValue[] = [tagId, bookId, userId, name, nameHash, color, System.timeStampInSeconds()]; - const result: RunResult = db.run(query, params); - if (!result || result.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du tag.` : `Error adding tag.`); - } - return tagId; + result = db.run(query, params); } catch (error: unknown) { if (error instanceof Error) { console.error(`[SpellTagRepo] DB Error: ${error.message}`); @@ -79,6 +76,10 @@ export default class SpellTagRepo { } throw new Error(lang === 'fr' ? `Impossible d'ajouter le tag de sort.` : `Unable to add spell tag.`); } + if (!result || result.changes === 0) { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du tag.` : `Error adding tag.`); + } + return tagId; } /** diff --git a/electron/database/repositories/world.repository.ts b/electron/database/repositories/world.repository.ts index 72385e1..831cf41 100644 --- a/electron/database/repositories/world.repository.ts +++ b/electron/database/repositories/world.repository.ts @@ -52,6 +52,7 @@ export interface WorldQuery extends Record { element_name: string | null; element_description: string | null; element_type: number | null; + series_world_id: string | null; } export interface WorldElementValue { @@ -98,12 +99,16 @@ export default class WorldRepository { * @param lang - The language for error messages ('fr' or 'en') * @returns The world ID if insertion was successful */ - public static insertNewWorld(worldId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en'): string { + public static insertNewWorld(worldId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en', seriesWorldId: string | null = null): string { let insertResult: RunResult; try { const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_world (world_id, author_id, book_id, name, hashed_name, last_update) VALUES (?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [worldId, userId, bookId, encryptedName, hashedName, System.timeStampInSeconds()]; + const query: string = seriesWorldId + ? 'INSERT INTO book_world (world_id, author_id, book_id, name, hashed_name, series_world_id, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)' + : 'INSERT INTO book_world (world_id, author_id, book_id, name, hashed_name, last_update) VALUES (?, ?, ?, ?, ?, ?)'; + const params: SQLiteValue[] = seriesWorldId + ? [worldId, userId, bookId, encryptedName, hashedName, seriesWorldId, System.timeStampInSeconds()] + : [worldId, userId, bookId, encryptedName, hashedName, System.timeStampInSeconds()]; insertResult = db.run(query, params); } catch (error: unknown) { if (error instanceof Error) { @@ -130,7 +135,7 @@ export default class WorldRepository { public static fetchWorlds(userId: string, bookId: string, lang: 'fr' | 'en'): WorldQuery[] { try { const db: Database = System.getDb(); - const query: string = `SELECT world.world_id AS world_id, world.name AS world_name, world.history, world.politics, world.economy, world.religion, world.languages, element.element_id AS element_id, element.name AS element_name, element.description AS element_description, element.element_type FROM book_world AS world LEFT JOIN book_world_elements AS element ON world.world_id=element.world_id WHERE world.author_id=? AND world.book_id=?`; + const query: string = 'SELECT world.world_id AS world_id, world.name AS world_name, world.history, world.politics, world.economy, world.religion, world.languages, element.element_id AS element_id, element.name AS element_name, element.description AS element_description, element.element_type, world.series_world_id FROM book_world AS world LEFT JOIN book_world_elements AS element ON world.world_id=element.world_id WHERE world.author_id=? AND world.book_id=?'; const params: SQLiteValue[] = [userId, bookId]; const worlds: WorldQuery[] = db.all(query, params) as WorldQuery[]; return worlds; @@ -160,11 +165,15 @@ export default class WorldRepository { * @param lang - The language for error messages ('fr' or 'en') * @returns True if the update was successful, false otherwise */ - public static updateWorld(userId: string, worldId: string, encryptName: string, hashedName: string, encryptHistory: string, encryptPolitics: string, encryptEconomy: string, encryptReligion: string, encryptLanguages: string, lastUpdate: number, lang: 'fr' | 'en'): boolean { + public static updateWorld(userId: string, worldId: string, encryptName: string, hashedName: string, encryptHistory: string, encryptPolitics: string, encryptEconomy: string, encryptReligion: string, encryptLanguages: string, lastUpdate: number, lang: 'fr' | 'en', seriesWorldId: string | null = null): boolean { try { const db: Database = System.getDb(); - const query: string = 'UPDATE book_world SET name=?, hashed_name=?, history=?, politics=?, economy=?, religion=?, languages=?, last_update=? WHERE author_id=? AND world_id=?'; - const params: SQLiteValue[] = [encryptName, hashedName, encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, lastUpdate, userId, worldId]; + const query: string = seriesWorldId !== null + ? 'UPDATE book_world SET name=?, hashed_name=?, history=?, politics=?, economy=?, religion=?, languages=?, last_update=?, series_world_id=? WHERE author_id=? AND world_id=?' + : 'UPDATE book_world SET name=?, hashed_name=?, history=?, politics=?, economy=?, religion=?, languages=?, last_update=? WHERE author_id=? AND world_id=?'; + const params: SQLiteValue[] = seriesWorldId !== null + ? [encryptName, hashedName, encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, lastUpdate, seriesWorldId, userId, worldId] + : [encryptName, hashedName, encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, lastUpdate, userId, worldId]; const updateResult: RunResult = db.run(query, params); return updateResult.changes > 0; } catch (error: unknown) { diff --git a/electron/database/schema.ts b/electron/database/schema.ts index ae2536a..3c76c9b 100644 --- a/electron/database/schema.ts +++ b/electron/database/schema.ts @@ -13,7 +13,7 @@ type Database = sqlite3.Database; // MIGRATIONS // ============================================================================= -const schemaVersion = 2; +const schemaVersion = 3; /** * DEV ONLY - S'exécute à chaque refresh, pas besoin de version @@ -137,6 +137,55 @@ function migrateFromOldSystem(db: Database): void { db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_book ON book_spells(book_id)`); db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_user ON book_spells(user_id)`); + // Create series tables (v3) + db.exec(`CREATE TABLE IF NOT EXISTS book_series (series_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, description TEXT, cover_image TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_book_series_user ON book_series(user_id)`); + + db.exec(`CREATE TABLE IF NOT EXISTS series_books (series_id TEXT NOT NULL, book_id TEXT NOT NULL, book_order INTEGER NOT NULL DEFAULT 1, last_update INTEGER DEFAULT 0, PRIMARY KEY (series_id, book_id), FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_books_book ON series_books(book_id)`); + + db.exec(`CREATE TABLE IF NOT EXISTS series_characters (character_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, first_name TEXT NOT NULL, last_name TEXT, nickname TEXT, age TEXT, gender TEXT, species TEXT, nationality TEXT, status TEXT, category TEXT NOT NULL, title TEXT, image TEXT, role TEXT, biography TEXT, history TEXT, speech_pattern TEXT, catchphrase TEXT, residence TEXT, notes TEXT, color TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_series ON series_characters(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_user ON series_characters(user_id)`); + + db.exec(`CREATE TABLE IF NOT EXISTS series_characters_attributes (attr_id TEXT PRIMARY KEY, character_id TEXT NOT NULL, user_id TEXT NOT NULL, attribute_name TEXT NOT NULL, attribute_value TEXT NOT NULL, last_update INTEGER DEFAULT 0, FOREIGN KEY (character_id) REFERENCES series_characters(character_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_character ON series_characters_attributes(character_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_user ON series_characters_attributes(user_id)`); + + db.exec(`CREATE TABLE IF NOT EXISTS series_worlds (world_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, history TEXT, politics TEXT, economy TEXT, religion TEXT, languages TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_series ON series_worlds(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_user ON series_worlds(user_id)`); + + db.exec(`CREATE TABLE IF NOT EXISTS series_world_elements (element_id TEXT PRIMARY KEY, world_id TEXT NOT NULL, user_id TEXT NOT NULL, element_type INTEGER NOT NULL, name TEXT NOT NULL, original_name TEXT NOT NULL, description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (world_id) REFERENCES series_worlds(world_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_world ON series_world_elements(world_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_user ON series_world_elements(user_id)`); + + db.exec(`CREATE TABLE IF NOT EXISTS series_locations (loc_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, loc_name TEXT NOT NULL, loc_original_name TEXT NOT NULL, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_series ON series_locations(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_user ON series_locations(user_id)`); + + db.exec(`CREATE TABLE IF NOT EXISTS series_location_elements (element_id TEXT PRIMARY KEY, location_id TEXT NOT NULL, user_id TEXT NOT NULL, element_name TEXT NOT NULL, original_name TEXT NOT NULL, element_description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (location_id) REFERENCES series_locations(loc_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_location ON series_location_elements(location_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_user ON series_location_elements(user_id)`); + + db.exec(`CREATE TABLE IF NOT EXISTS series_location_sub_elements (sub_element_id TEXT PRIMARY KEY, element_id TEXT NOT NULL, user_id TEXT NOT NULL, sub_elem_name TEXT NOT NULL, original_name TEXT NOT NULL, sub_elem_description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (element_id) REFERENCES series_location_elements(element_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`); + + db.exec(`CREATE TABLE IF NOT EXISTS series_spells (spell_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, name_hash TEXT NOT NULL, description TEXT NOT NULL, appearance TEXT NOT NULL, tags TEXT, power_level TEXT, components TEXT, limitations TEXT, notes TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`); + + db.exec(`CREATE TABLE IF NOT EXISTS series_spell_tags (tag_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, color TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)`); + + // Add series_*_id columns to existing book tables (v3) + addColumn(db, 'book_characters', 'series_character_id', 'TEXT DEFAULT NULL'); + addColumn(db, 'book_world', 'series_world_id', 'TEXT DEFAULT NULL'); + addColumn(db, 'book_location', 'series_location_id', 'TEXT DEFAULT NULL'); + addColumn(db, 'book_spells', 'series_spell_id', 'TEXT DEFAULT NULL'); + // Drop old schema version table db.exec('DROP TABLE IF EXISTS _schema_version'); } @@ -236,6 +285,70 @@ export function runMigrations(db: Database): void { addColumn(db, 'book_characters', 'color', 'TEXT DEFAULT NULL'); } + // v3 - Add series tables and series_*_id columns to existing book tables + if (currentVersion < 3) { + // Create series tables first (order matters for foreign keys) + + // Book Series (main series table) + db.exec(`CREATE TABLE IF NOT EXISTS book_series (series_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, description TEXT, cover_image TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_book_series_user ON book_series(user_id)`); + + // Series Books (link series to books with order) + db.exec(`CREATE TABLE IF NOT EXISTS series_books (series_id TEXT NOT NULL, book_id TEXT NOT NULL, book_order INTEGER NOT NULL DEFAULT 1, last_update INTEGER DEFAULT 0, PRIMARY KEY (series_id, book_id), FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_books_book ON series_books(book_id)`); + + // Series Characters + db.exec(`CREATE TABLE IF NOT EXISTS series_characters (character_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, first_name TEXT NOT NULL, last_name TEXT, nickname TEXT, age TEXT, gender TEXT, species TEXT, nationality TEXT, status TEXT, category TEXT NOT NULL, title TEXT, image TEXT, role TEXT, biography TEXT, history TEXT, speech_pattern TEXT, catchphrase TEXT, residence TEXT, notes TEXT, color TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_series ON series_characters(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_user ON series_characters(user_id)`); + + // Series Characters Attributes + db.exec(`CREATE TABLE IF NOT EXISTS series_characters_attributes (attr_id TEXT PRIMARY KEY, character_id TEXT NOT NULL, user_id TEXT NOT NULL, attribute_name TEXT NOT NULL, attribute_value TEXT NOT NULL, last_update INTEGER DEFAULT 0, FOREIGN KEY (character_id) REFERENCES series_characters(character_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_character ON series_characters_attributes(character_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_user ON series_characters_attributes(user_id)`); + + // Series Worlds + db.exec(`CREATE TABLE IF NOT EXISTS series_worlds (world_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, history TEXT, politics TEXT, economy TEXT, religion TEXT, languages TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_series ON series_worlds(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_user ON series_worlds(user_id)`); + + // Series World Elements + db.exec(`CREATE TABLE IF NOT EXISTS series_world_elements (element_id TEXT PRIMARY KEY, world_id TEXT NOT NULL, user_id TEXT NOT NULL, element_type INTEGER NOT NULL, name TEXT NOT NULL, original_name TEXT NOT NULL, description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (world_id) REFERENCES series_worlds(world_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_world ON series_world_elements(world_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_user ON series_world_elements(user_id)`); + + // Series Locations + db.exec(`CREATE TABLE IF NOT EXISTS series_locations (loc_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, loc_name TEXT NOT NULL, loc_original_name TEXT NOT NULL, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_series ON series_locations(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_user ON series_locations(user_id)`); + + // Series Location Elements + db.exec(`CREATE TABLE IF NOT EXISTS series_location_elements (element_id TEXT PRIMARY KEY, location_id TEXT NOT NULL, user_id TEXT NOT NULL, element_name TEXT NOT NULL, original_name TEXT NOT NULL, element_description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (location_id) REFERENCES series_locations(loc_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_location ON series_location_elements(location_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_user ON series_location_elements(user_id)`); + + // Series Location Sub Elements + db.exec(`CREATE TABLE IF NOT EXISTS series_location_sub_elements (sub_element_id TEXT PRIMARY KEY, element_id TEXT NOT NULL, user_id TEXT NOT NULL, sub_elem_name TEXT NOT NULL, original_name TEXT NOT NULL, sub_elem_description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (element_id) REFERENCES series_location_elements(element_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`); + + // Series Spells + db.exec(`CREATE TABLE IF NOT EXISTS series_spells (spell_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, name_hash TEXT NOT NULL, description TEXT NOT NULL, appearance TEXT NOT NULL, tags TEXT, power_level TEXT, components TEXT, limitations TEXT, notes TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`); + + // Series Spell Tags + db.exec(`CREATE TABLE IF NOT EXISTS series_spell_tags (tag_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, color TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)`); + + // Add series_*_id columns to existing book tables + addColumn(db, 'book_characters', 'series_character_id', 'TEXT DEFAULT NULL'); + addColumn(db, 'book_world', 'series_world_id', 'TEXT DEFAULT NULL'); + addColumn(db, 'book_location', 'series_location_id', 'TEXT DEFAULT NULL'); + addColumn(db, 'book_spells', 'series_spell_id', 'TEXT DEFAULT NULL'); + } + setDbVersion(db, schemaVersion); } @@ -391,8 +504,10 @@ export function initializeSchema(db: Database): void { residence TEXT, notes TEXT, color TEXT, + series_character_id TEXT, last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE + FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE, + FOREIGN KEY (series_character_id) REFERENCES series_characters(character_id) ON DELETE SET NULL ); `); @@ -478,8 +593,10 @@ export function initializeSchema(db: Database): void { user_id TEXT NOT NULL, loc_name TEXT NOT NULL, loc_original_name TEXT NOT NULL, + series_location_id TEXT, last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE + FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE, + FOREIGN KEY (series_location_id) REFERENCES series_locations(loc_id) ON DELETE SET NULL ); `); @@ -511,8 +628,10 @@ export function initializeSchema(db: Database): void { economy TEXT, religion TEXT, languages TEXT, + series_world_id TEXT, last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE + FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE, + FOREIGN KEY (series_world_id) REFERENCES series_worlds(world_id) ON DELETE SET NULL ); `); @@ -696,11 +815,195 @@ export function initializeSchema(db: Database): void { components TEXT, limitations TEXT, notes TEXT, + series_spell_id TEXT, last_update INTEGER DEFAULT 0, + FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE, + FOREIGN KEY (series_spell_id) REFERENCES series_spells(spell_id) ON DELETE SET NULL + ); + `); + + // ============================================================================= + // SERIES TABLES + // ============================================================================= + + // Book Series (main series table) + db.exec(` + CREATE TABLE IF NOT EXISTS book_series ( + series_id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + hashed_name TEXT NOT NULL, + description TEXT, + cover_image TEXT, + last_update INTEGER DEFAULT 0, + FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE + ); + `); + + // Series Books (link series to books with order) + db.exec(` + CREATE TABLE IF NOT EXISTS series_books ( + series_id TEXT NOT NULL, + book_id TEXT NOT NULL, + book_order INTEGER NOT NULL DEFAULT 1, + last_update INTEGER DEFAULT 0, + PRIMARY KEY (series_id, book_id), + FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); + // Series Characters + db.exec(` + CREATE TABLE IF NOT EXISTS series_characters ( + character_id TEXT PRIMARY KEY, + series_id TEXT NOT NULL, + user_id TEXT NOT NULL, + first_name TEXT NOT NULL, + last_name TEXT, + nickname TEXT, + age TEXT, + gender TEXT, + species TEXT, + nationality TEXT, + status TEXT, + category TEXT NOT NULL, + title TEXT, + image TEXT, + role TEXT, + biography TEXT, + history TEXT, + speech_pattern TEXT, + catchphrase TEXT, + residence TEXT, + notes TEXT, + color TEXT, + last_update INTEGER DEFAULT 0, + FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE + ); + `); + + // Series Characters Attributes + db.exec(` + CREATE TABLE IF NOT EXISTS series_characters_attributes ( + attr_id TEXT PRIMARY KEY, + character_id TEXT NOT NULL, + user_id TEXT NOT NULL, + attribute_name TEXT NOT NULL, + attribute_value TEXT NOT NULL, + last_update INTEGER DEFAULT 0, + FOREIGN KEY (character_id) REFERENCES series_characters(character_id) ON DELETE CASCADE + ); + `); + + // Series Worlds + db.exec(` + CREATE TABLE IF NOT EXISTS series_worlds ( + world_id TEXT PRIMARY KEY, + series_id TEXT NOT NULL, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + hashed_name TEXT NOT NULL, + history TEXT, + politics TEXT, + economy TEXT, + religion TEXT, + languages TEXT, + last_update INTEGER DEFAULT 0, + FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE + ); + `); + + // Series World Elements + db.exec(` + CREATE TABLE IF NOT EXISTS series_world_elements ( + element_id TEXT PRIMARY KEY, + world_id TEXT NOT NULL, + user_id TEXT NOT NULL, + element_type INTEGER NOT NULL, + name TEXT NOT NULL, + original_name TEXT NOT NULL, + description TEXT, + last_update INTEGER DEFAULT 0, + FOREIGN KEY (world_id) REFERENCES series_worlds(world_id) ON DELETE CASCADE + ); + `); + + // Series Locations + db.exec(` + CREATE TABLE IF NOT EXISTS series_locations ( + loc_id TEXT PRIMARY KEY, + series_id TEXT NOT NULL, + user_id TEXT NOT NULL, + loc_name TEXT NOT NULL, + loc_original_name TEXT NOT NULL, + last_update INTEGER DEFAULT 0, + FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE + ); + `); + + // Series Location Elements + db.exec(` + CREATE TABLE IF NOT EXISTS series_location_elements ( + element_id TEXT PRIMARY KEY, + location_id TEXT NOT NULL, + user_id TEXT NOT NULL, + element_name TEXT NOT NULL, + original_name TEXT NOT NULL, + element_description TEXT, + last_update INTEGER DEFAULT 0, + FOREIGN KEY (location_id) REFERENCES series_locations(loc_id) ON DELETE CASCADE + ); + `); + + // Series Location Sub Elements + db.exec(` + CREATE TABLE IF NOT EXISTS series_location_sub_elements ( + sub_element_id TEXT PRIMARY KEY, + element_id TEXT NOT NULL, + user_id TEXT NOT NULL, + sub_elem_name TEXT NOT NULL, + original_name TEXT NOT NULL, + sub_elem_description TEXT, + last_update INTEGER DEFAULT 0, + FOREIGN KEY (element_id) REFERENCES series_location_elements(element_id) ON DELETE CASCADE + ); + `); + + // Series Spells + db.exec(` + CREATE TABLE IF NOT EXISTS series_spells ( + spell_id TEXT PRIMARY KEY, + series_id TEXT NOT NULL, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + name_hash TEXT NOT NULL, + description TEXT NOT NULL, + appearance TEXT NOT NULL, + tags TEXT, + power_level TEXT, + components TEXT, + limitations TEXT, + notes TEXT, + last_update INTEGER DEFAULT 0, + FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE + ); + `); + + // Series Spell Tags + db.exec(` + CREATE TABLE IF NOT EXISTS series_spell_tags ( + tag_id TEXT PRIMARY KEY, + series_id TEXT NOT NULL, + user_id TEXT NOT NULL, + name TEXT NOT NULL, + hashed_name TEXT NOT NULL, + color TEXT, + last_update INTEGER DEFAULT 0, + FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE + ); + `); + // Create indexes for better performance createIndexes(db); } @@ -724,6 +1027,28 @@ function createIndexes(db: Database): void { db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_user ON book_spell_tags(user_id)`); db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_book ON book_spells(book_id)`); db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_user ON book_spells(user_id)`); + + // Series tables indexes + db.exec(`CREATE INDEX IF NOT EXISTS idx_book_series_user ON book_series(user_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_books_book ON series_books(book_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_series ON series_characters(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_user ON series_characters(user_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_character ON series_characters_attributes(character_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_user ON series_characters_attributes(user_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_series ON series_worlds(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_user ON series_worlds(user_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_world ON series_world_elements(world_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_user ON series_world_elements(user_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_series ON series_locations(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_user ON series_locations(user_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_location ON series_location_elements(location_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_user ON series_location_elements(user_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)`); } /** diff --git a/electron/ipc/book.ipc.ts b/electron/ipc/book.ipc.ts index 80f1293..317f4e9 100644 --- a/electron/ipc/book.ipc.ts +++ b/electron/ipc/book.ipc.ts @@ -82,6 +82,7 @@ interface AddWorldData { bookId: string; worldName: string; id?: string; + seriesWorldId?: string | null; } interface AddWorldElementData { @@ -113,7 +114,7 @@ interface UpdateWorldData { interface UpdateBookToolData { bookId: string; - toolName: 'characters' | 'worlds' | 'locations'; + toolName: 'characters' | 'worlds' | 'locations' | 'spells'; enabled: boolean; } @@ -340,7 +341,7 @@ ipcMain.handle('db:book:worlds:get', createHandler( function(userId: string, data: AddWorldData, lang: 'fr' | 'en') { - return World.addNewWorld(userId, data.bookId, data.worldName, lang, data.id); + return World.addNewWorld(userId, data.bookId, data.worldName, lang, data.id, data.seriesWorldId || null); } ) ); diff --git a/electron/ipc/location.ipc.ts b/electron/ipc/location.ipc.ts index 3adc741..24a447c 100644 --- a/electron/ipc/location.ipc.ts +++ b/electron/ipc/location.ipc.ts @@ -12,6 +12,7 @@ interface AddLocationSectionData { locationName: string; bookId: string; id?: string; + seriesLocationId?: string | null; } interface AddLocationElementData { @@ -44,7 +45,7 @@ ipcMain.handle('db:location:all', createHandler( function(userId: string, data: AddLocationSectionData, lang: 'fr' | 'en'): string { - return Location.addLocationSection(userId, data.locationName, data.bookId, lang, data.id); + return Location.addLocationSection(userId, data.locationName, data.bookId, lang, data.id, data.seriesLocationId || null); } ) ); @@ -73,6 +74,19 @@ ipcMain.handle('db:location:update', createHandler( + function(userId: string, data: UpdateSectionWithSeriesLinkData, lang: 'fr' | 'en'): boolean { + return Location.updateSectionWithSeriesLink(userId, data.sectionId, data.sectionName, data.seriesLocationId, lang); + } + ) +); + // DELETE /location/delete - Delete location section interface DeleteLocationData { locationId: string; diff --git a/electron/ipc/series-character.ipc.ts b/electron/ipc/series-character.ipc.ts new file mode 100644 index 0000000..cbaea36 --- /dev/null +++ b/electron/ipc/series-character.ipc.ts @@ -0,0 +1,83 @@ +import { ipcMain } from 'electron'; +import { createHandler } from '../database/LocalSystem.js'; +import SeriesCharacter, { SeriesCharacterPropsPost, SeriesCharacterListProps, CharacterAttributesResponse } from '../database/models/SeriesCharacter.js'; + +interface GetCharacterListData { + seriesId: string; +} + +interface GetCharacterAttributesData { + characterId: string; +} + +interface AddCharacterData { + seriesId: string; + character: SeriesCharacterPropsPost; +} + +interface UpdateCharacterData { + character: SeriesCharacterPropsPost; +} + +interface DeleteCharacterData { + characterId: string; +} + +interface AddAttributeData { + characterId: string; + type: string; + name: string; +} + +interface DeleteAttributeData { + attributeId: string; +} + +// GET /series/character/list - Get character list +ipcMain.handle('db:series:character:list', createHandler( + function(userId: string, data: GetCharacterListData, lang: 'fr' | 'en'): SeriesCharacterListProps[] { + return SeriesCharacter.getCharacterList(userId, data.seriesId, lang); + } +)); + +// GET /series/character/attribute - Get character attributes +ipcMain.handle('db:series:character:attributes', createHandler( + function(userId: string, data: GetCharacterAttributesData, lang: 'fr' | 'en'): CharacterAttributesResponse { + return SeriesCharacter.getCharacterAttributes(userId, data.characterId, lang); + } +)); + +// POST /series/character/add - Add new character +ipcMain.handle('db:series:character:add', createHandler( + function(userId: string, data: AddCharacterData, lang: 'fr' | 'en'): string { + return SeriesCharacter.addNewCharacter(userId, data.character, data.seriesId, lang); + } +)); + +// PATCH /series/character/update - Update character +ipcMain.handle('db:series:character:update', createHandler( + function(userId: string, data: UpdateCharacterData, lang: 'fr' | 'en'): boolean { + return SeriesCharacter.updateCharacter(userId, data.character, lang); + } +)); + +// DELETE /series/character/delete - Delete character +ipcMain.handle('db:series:character:delete', createHandler( + function(userId: string, data: DeleteCharacterData, lang: 'fr' | 'en'): boolean { + return SeriesCharacter.deleteCharacter(userId, data.characterId, lang); + } +)); + +// POST /series/character/attribute/add - Add attribute +ipcMain.handle('db:series:character:attribute:add', createHandler( + function(userId: string, data: AddAttributeData, lang: 'fr' | 'en'): string { + return SeriesCharacter.addNewAttribute(data.characterId, userId, data.type, data.name, lang); + } +)); + +// DELETE /series/character/attribute/delete - Delete attribute +ipcMain.handle('db:series:character:attribute:delete', createHandler( + function(userId: string, data: DeleteAttributeData, lang: 'fr' | 'en'): boolean { + return SeriesCharacter.deleteAttribute(userId, data.attributeId, lang); + } +)); diff --git a/electron/ipc/series-location.ipc.ts b/electron/ipc/series-location.ipc.ts new file mode 100644 index 0000000..3f23d5f --- /dev/null +++ b/electron/ipc/series-location.ipc.ts @@ -0,0 +1,85 @@ +import { ipcMain } from 'electron'; +import { createHandler } from '../database/LocalSystem.js'; +import SeriesLocation, { SeriesLocationListProps } from '../database/models/SeriesLocation.js'; + +interface GetLocationListData { + seriesId: string; +} + +interface AddLocationSectionData { + seriesId: string; + name: string; +} + +interface AddElementData { + locationId: string; + name: string; + description?: string; +} + +interface AddSubElementData { + elementId: string; + name: string; + description?: string; +} + +interface DeleteLocationData { + locationId: string; +} + +interface DeleteElementData { + elementId: string; +} + +interface DeleteSubElementData { + subElementId: string; +} + +// GET /series/location/list - Get location list +ipcMain.handle('db:series:location:list', createHandler( + function(userId: string, data: GetLocationListData, lang: 'fr' | 'en'): SeriesLocationListProps[] { + return SeriesLocation.getLocationList(userId, data.seriesId, lang); + } +)); + +// POST /series/location/section/add - Add location section +ipcMain.handle('db:series:location:section:add', createHandler( + function(userId: string, data: AddLocationSectionData, lang: 'fr' | 'en'): string { + return SeriesLocation.addLocationSection(userId, data.seriesId, data.name, lang); + } +)); + +// POST /series/location/element/add - Add element +ipcMain.handle('db:series:location:element:add', createHandler( + function(userId: string, data: AddElementData, lang: 'fr' | 'en'): string { + return SeriesLocation.addElement(userId, data.locationId, data.name, lang, data.description); + } +)); + +// POST /series/location/sub-element/add - Add sub-element +ipcMain.handle('db:series:location:subelement:add', createHandler( + function(userId: string, data: AddSubElementData, lang: 'fr' | 'en'): string { + return SeriesLocation.addSubElement(userId, data.elementId, data.name, lang, data.description); + } +)); + +// DELETE /series/location/delete - Delete location +ipcMain.handle('db:series:location:delete', createHandler( + function(userId: string, data: DeleteLocationData, lang: 'fr' | 'en'): boolean { + return SeriesLocation.deleteLocation(userId, data.locationId, lang); + } +)); + +// DELETE /series/location/element/delete - Delete element +ipcMain.handle('db:series:location:element:delete', createHandler( + function(userId: string, data: DeleteElementData, lang: 'fr' | 'en'): boolean { + return SeriesLocation.deleteElement(userId, data.elementId, lang); + } +)); + +// DELETE /series/location/sub-element/delete - Delete sub-element +ipcMain.handle('db:series:location:subelement:delete', createHandler( + function(userId: string, data: DeleteSubElementData, lang: 'fr' | 'en'): boolean { + return SeriesLocation.deleteSubElement(userId, data.subElementId, lang); + } +)); diff --git a/electron/ipc/series-spell.ipc.ts b/electron/ipc/series-spell.ipc.ts new file mode 100644 index 0000000..2bf2db3 --- /dev/null +++ b/electron/ipc/series-spell.ipc.ts @@ -0,0 +1,111 @@ +import { ipcMain } from 'electron'; +import { createHandler } from '../database/LocalSystem.js'; +import SeriesSpell, { SeriesSpellListResponse, SeriesSpellDetailProps } from '../database/models/SeriesSpell.js'; + +interface GetSpellListData { + seriesId: string; +} + +interface GetSpellDetailData { + spellId: string; +} + +interface AddSpellData { + seriesId: string; + name: string; + description?: string | null; + appearance?: string | null; + tags?: string[]; + powerLevel?: string | null; + components?: string | null; + limitations?: string | null; + notes?: string | null; +} + +interface UpdateSpellData { + id: string; + name: string; + description?: string | null; + appearance?: string | null; + tags?: string[]; + powerLevel?: string | null; + components?: string | null; + limitations?: string | null; + notes?: string | null; +} + +interface DeleteSpellData { + spellId: string; +} + +interface AddTagData { + seriesId: string; + name: string; + color?: string | null; +} + +interface UpdateTagData { + tagId: string; + name: string; + color?: string | null; +} + +interface DeleteTagData { + tagId: string; +} + +// GET /series/spell/list - Get spell list +ipcMain.handle('db:series:spell:list', createHandler( + function(userId: string, data: GetSpellListData, lang: 'fr' | 'en'): SeriesSpellListResponse { + return SeriesSpell.getSpellList(userId, data.seriesId, lang); + } +)); + +// GET /series/spell/detail - Get spell detail +ipcMain.handle('db:series:spell:detail', createHandler( + function(userId: string, data: GetSpellDetailData, lang: 'fr' | 'en'): SeriesSpellDetailProps { + return SeriesSpell.getSpellDetail(userId, data.spellId, lang); + } +)); + +// POST /series/spell/add - Add spell +ipcMain.handle('db:series:spell:add', createHandler( + function(userId: string, data: AddSpellData, lang: 'fr' | 'en'): string { + return SeriesSpell.addSpell(userId, data.seriesId, data.name, lang, data.description, data.appearance, data.tags, data.powerLevel, data.components, data.limitations, data.notes); + } +)); + +// PUT /series/spell/update - Update spell +ipcMain.handle('db:series:spell:update', createHandler( + function(userId: string, data: UpdateSpellData, lang: 'fr' | 'en'): boolean { + return SeriesSpell.updateSpell(userId, data.id, data.name, lang, data.description, data.appearance, data.tags, data.powerLevel, data.components, data.limitations, data.notes); + } +)); + +// DELETE /series/spell/delete - Delete spell +ipcMain.handle('db:series:spell:delete', createHandler( + function(userId: string, data: DeleteSpellData, lang: 'fr' | 'en'): boolean { + return SeriesSpell.deleteSpell(userId, data.spellId, lang); + } +)); + +// POST /series/spell/tag/add - Add tag +ipcMain.handle('db:series:spell:tag:add', createHandler( + function(userId: string, data: AddTagData, lang: 'fr' | 'en'): string { + return SeriesSpell.addTag(userId, data.seriesId, data.name, lang, data.color); + } +)); + +// PUT /series/spell/tag/update - Update tag +ipcMain.handle('db:series:spell:tag:update', createHandler( + function(userId: string, data: UpdateTagData, lang: 'fr' | 'en'): boolean { + return SeriesSpell.updateTag(userId, data.tagId, data.name, lang, data.color); + } +)); + +// DELETE /series/spell/tag/delete - Delete tag +ipcMain.handle('db:series:spell:tag:delete', createHandler( + function(userId: string, data: DeleteTagData, lang: 'fr' | 'en'): boolean { + return SeriesSpell.deleteTag(userId, data.tagId, lang); + } +)); diff --git a/electron/ipc/series-sync.ipc.ts b/electron/ipc/series-sync.ipc.ts new file mode 100644 index 0000000..ecb8891 --- /dev/null +++ b/electron/ipc/series-sync.ipc.ts @@ -0,0 +1,24 @@ +import { ipcMain } from 'electron'; +import { createHandler } from '../database/LocalSystem.js'; +import SeriesSync, { SeriesSyncUploadPayload, SeriesSyncResult } from '../database/models/SeriesSync.js'; +import { SyncElementType } from '../database/repositories/series-sync.repo.js'; + +interface UploadToSeriesData { + type: SyncElementType; + bookElementId: string; + field: string; + value: string; +} + +// POST /series/sync/upload - Upload field to series +ipcMain.handle('db:series:sync:upload', createHandler( + function(userId: string, data: UploadToSeriesData, lang: 'fr' | 'en'): SeriesSyncResult { + const payload: SeriesSyncUploadPayload = { + type: data.type, + bookElementId: data.bookElementId, + field: data.field, + value: data.value || '' + }; + return SeriesSync.uploadFieldToSeries(userId, payload, lang); + } +)); diff --git a/electron/ipc/series-world.ipc.ts b/electron/ipc/series-world.ipc.ts new file mode 100644 index 0000000..2323a95 --- /dev/null +++ b/electron/ipc/series-world.ipc.ts @@ -0,0 +1,76 @@ +import { ipcMain } from 'electron'; +import { createHandler } from '../database/LocalSystem.js'; +import SeriesWorld, { SeriesWorldListProps, SeriesWorldUpdateProps } from '../database/models/SeriesWorld.js'; + +interface GetWorldListData { + seriesId: string; +} + +interface AddWorldData { + seriesId: string; + name: string; +} + +interface UpdateWorldData { + worldId: string; + name: string; + history?: string; + politics?: string; + economy?: string; + religion?: string; + languages?: string; +} + +interface AddElementData { + worldId: string; + elementType: number; + name: string; + description?: string; +} + +interface DeleteElementData { + elementId: string; +} + +// GET /series/world/list - Get world list +ipcMain.handle('db:series:world:list', createHandler( + function(userId: string, data: GetWorldListData, lang: 'fr' | 'en'): SeriesWorldListProps[] { + return SeriesWorld.getWorldList(userId, data.seriesId, lang); + } +)); + +// POST /series/world/add - Add world +ipcMain.handle('db:series:world:add', createHandler( + function(userId: string, data: AddWorldData, lang: 'fr' | 'en'): string { + return SeriesWorld.addWorld(userId, data.seriesId, data.name, lang); + } +)); + +// PATCH /series/world/update - Update world +ipcMain.handle('db:series:world:update', createHandler( + function(userId: string, data: UpdateWorldData, lang: 'fr' | 'en'): boolean { + const worldData: SeriesWorldUpdateProps = { + name: data.name, + history: data.history, + politics: data.politics, + economy: data.economy, + religion: data.religion, + languages: data.languages + }; + return SeriesWorld.updateWorld(userId, data.worldId, worldData, lang); + } +)); + +// POST /series/world/element/add - Add element +ipcMain.handle('db:series:world:element:add', createHandler( + function(userId: string, data: AddElementData, lang: 'fr' | 'en'): string { + return SeriesWorld.addElement(userId, data.worldId, data.elementType, data.name, lang, data.description); + } +)); + +// DELETE /series/world/element/delete - Delete element +ipcMain.handle('db:series:world:element:delete', createHandler( + function(userId: string, data: DeleteElementData, lang: 'fr' | 'en'): boolean { + return SeriesWorld.deleteElement(userId, data.elementId, lang); + } +)); diff --git a/electron/ipc/series.ipc.ts b/electron/ipc/series.ipc.ts new file mode 100644 index 0000000..8f20e89 --- /dev/null +++ b/electron/ipc/series.ipc.ts @@ -0,0 +1,117 @@ +import { ipcMain } from 'electron'; +import { createHandler } from '../database/LocalSystem.js'; +import Series, { BooksOrderPost, SeriesDetailProps, SeriesListItemProps, SeriesBookProps } from '../database/models/Series.js'; + +interface CreateSeriesData { + name: string; + description?: string; + bookIds?: string[]; +} + +interface UpdateSeriesData { + seriesId: string; + name: string; + description?: string; +} + +interface DeleteSeriesData { + seriesId: string; +} + +interface GetSeriesDetailData { + seriesId: string; +} + +interface AddBookToSeriesData { + seriesId: string; + bookId: string; + order?: number; +} + +interface RemoveBookFromSeriesData { + seriesId: string; + bookId: string; +} + +interface UpdateBooksOrderData { + seriesId: string; + booksOrder: BooksOrderPost[]; +} + +interface GetSeriesForBookData { + bookId: string; +} + +interface GetSeriesBooksData { + seriesId: string; +} + +// GET /series/list - Get all series +ipcMain.handle('db:series:list', createHandler( + async function(userId: string, _body: void, lang: 'fr' | 'en'): Promise { + return await Series.getSeriesList(userId, lang); + } +)); + +// GET /series/detail - Get series detail +ipcMain.handle('db:series:detail', createHandler( + async function(userId: string, data: GetSeriesDetailData, lang: 'fr' | 'en'): Promise { + return await Series.getSeriesDetail(userId, data.seriesId, lang); + } +)); + +// POST /series/add - Create new series +ipcMain.handle('db:series:create', createHandler( + async function(userId: string, data: CreateSeriesData, lang: 'fr' | 'en'): Promise { + return await Series.createSeries(userId, data.name, data.description || '', lang, data.bookIds); + } +)); + +// PUT /series/update - Update series +ipcMain.handle('db:series:update', createHandler( + async function(userId: string, data: UpdateSeriesData, lang: 'fr' | 'en'): Promise { + return await Series.updateSeries(userId, data.seriesId, data.name, data.description || '', lang); + } +)); + +// DELETE /series/delete - Delete series +ipcMain.handle('db:series:delete', createHandler( + async function(userId: string, data: DeleteSeriesData, lang: 'fr' | 'en'): Promise { + return await Series.deleteSeries(userId, data.seriesId, lang); + } +)); + +// GET /series/book/list - Get books in series +ipcMain.handle('db:series:books', createHandler( + async function(userId: string, data: GetSeriesBooksData, lang: 'fr' | 'en'): Promise { + return await Series.getSeriesBooks(userId, data.seriesId, lang); + } +)); + +// POST /series/book/add - Add book to series +ipcMain.handle('db:series:book:add', createHandler( + async function(userId: string, data: AddBookToSeriesData, lang: 'fr' | 'en'): Promise { + return await Series.addBookToSeries(userId, data.seriesId, data.bookId, data.order ?? 1, lang); + } +)); + +// DELETE /series/book/remove - Remove book from series +ipcMain.handle('db:series:book:remove', createHandler( + async function(userId: string, data: RemoveBookFromSeriesData, lang: 'fr' | 'en'): Promise { + return await Series.removeBookFromSeries(userId, data.seriesId, data.bookId, lang); + } +)); + +// PUT /series/book/reorder - Reorder books in series +ipcMain.handle('db:series:book:reorder', createHandler( + async function(userId: string, data: UpdateBooksOrderData, lang: 'fr' | 'en'): Promise { + return await Series.updateBooksOrder(userId, data.seriesId, data.booksOrder, lang); + } +)); + +// GET /series/for-book - Get series ID for a book +ipcMain.handle('db:series:forBook', createHandler( + function(_userId: string, data: GetSeriesForBookData, _lang: 'fr' | 'en'): string | null { + return Series.getSeriesIdForBook(data.bookId); + } +)); diff --git a/electron/ipc/spell.ipc.ts b/electron/ipc/spell.ipc.ts index b07f118..27693d0 100644 --- a/electron/ipc/spell.ipc.ts +++ b/electron/ipc/spell.ipc.ts @@ -19,6 +19,7 @@ interface SpellPost { components?: string | null; limitations?: string | null; notes?: string | null; + seriesSpellId?: string | null; } interface GetSpellListData { @@ -115,6 +116,7 @@ ipcMain.handle( spell.notes || null, spell.id, lang, + spell.seriesSpellId || null, ); return result.id; }, @@ -139,6 +141,7 @@ ipcMain.handle( spell.limitations || null, spell.notes || null, lang, + spell.seriesSpellId || null, ); }, ),