import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; import System from "../System.js"; export interface BookCharactersTable extends Record { character_id: string; book_id: string; user_id: string; first_name: string; last_name: string | null; category: string; title: string | null; image: string | null; role: string | null; biography: string | null; history: string | null; last_update: number; } export interface SyncedCharacterResult extends Record { character_id: string; book_id: string; first_name: string; last_update: number; } export interface SyncedCharacterAttributeResult extends Record { attr_id: string; character_id: string; attribute_name: string; last_update: number; } export interface BookCharactersAttributesTable extends Record { attr_id: string; character_id: string; user_id: string; attribute_name: string; attribute_value: string; last_update: number; } export interface CharacterResult extends Record { character_id: string; first_name: string; last_name: string; title: string; category: string; image: string; role: string; biography: string; history: string; } export interface AttributeResult extends Record { attr_id: string; attribute_name: string; attribute_value: string; } export interface CompleteCharacterResult extends Record { character_id: string; first_name: string; last_name: string; category: string; title: string; role: string; biography: string; history: string; attribute_name: string; attribute_value: string; } export default class CharacterRepo { /** * Fetches all characters for a specific book and user. * @param userId - The unique identifier of the user * @param bookId - The unique identifier of the book * @param lang - The language for error messages ('fr' or 'en') * @returns An array of character results */ 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, title, category, image, role, biography, history 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; } 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.` : `Unable to retrieve 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 the database. * @param userId - The unique identifier of the user * @param characterId - The unique identifier for the new character * @param encryptedName - The encrypted first name of the character * @param encryptedLastName - The encrypted last name of the character * @param encryptedTitle - The encrypted title of the character * @param encryptedCategory - The encrypted category of the character * @param encryptedImage - The encrypted image path of the character * @param encryptedRole - The encrypted role of the character * @param encryptedBiography - The encrypted biography of the character * @param encryptedHistory - The encrypted history of the character * @param bookId - The unique identifier of the book * @param lang - The language for error messages ('fr' or 'en') * @returns The character ID if successful */ public static addNewCharacter(userId: string, characterId: string, encryptedName: string, encryptedLastName: string, encryptedTitle: string, encryptedCategory: string, encryptedImage: string, encryptedRole: string, encryptedBiography: string, encryptedHistory: string, bookId: string, lang: 'fr' | 'en' = 'fr'): string { try { const db: Database = System.getDb(); const query: string = 'INSERT INTO `book_characters` (character_id, book_id, user_id, first_name, last_name, category, title, image, role, biography, history, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)'; const params: SQLiteValue[] = [characterId, bookId, userId, encryptedName, encryptedLastName, encryptedCategory, encryptedTitle, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, 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; } 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."); } } } /** * Inserts a new attribute for a character. * @param attributeId - The unique identifier for the new attribute * @param characterId - The unique identifier of the character * @param userId - The unique identifier of the user * @param type - The attribute name/type * @param name - The attribute value * @param lang - The language for error messages ('fr' or 'en') * @returns The attribute ID if successful */ static insertAttribute(attributeId: string, characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr'): string { 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 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; } 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."); } } } /** * Updates an existing character's information. * @param userId - The unique identifier of the user * @param id - The unique identifier of the character to update * @param encryptedName - The encrypted first name of the character * @param encryptedLastName - The encrypted last name of the character * @param encryptedTitle - The encrypted title of the character * @param encryptedCategory - The encrypted category of the character * @param encryptedImage - The encrypted image path of the character * @param encryptedRole - The encrypted role of the character * @param encryptedBiography - The encrypted biography of the character * @param encryptedHistory - The encrypted history of the character * @param lastUpdate - The timestamp of the last update * @param lang - The language for error messages ('fr' or 'en') * @returns True if the update was successful, false otherwise */ static updateCharacter(userId: string, id: string, encryptedName: string, encryptedLastName: string, encryptedTitle: string, encryptedCategory: string, encryptedImage: string, encryptedRole: string, encryptedBiography: string, encryptedHistory: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const query: string = 'UPDATE `book_characters` SET `first_name`=?,`last_name`=?,`title`=?,`category`=?,`image`=?,`role`=?,`biography`=?,`history`=?,`last_update`=? WHERE `character_id`=? AND `user_id`=?'; const params: SQLiteValue[] = [encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, lastUpdate, id, 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 character and all its related data (attributes) from the database. * @param userId - The unique identifier of the user * @param characterId - The unique identifier of the character to delete * @param lang - The language for error messages ('fr' or 'en') * @returns True if the deletion was successful, false otherwise */ static deleteCharacter(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const deleteAttributesQuery: string = 'DELETE FROM `book_characters_attributes` WHERE `character_id`=? AND `user_id`=?'; db.run(deleteAttributesQuery, [characterId, userId]); const deleteCharacterQuery: string = 'DELETE FROM `book_characters` WHERE `character_id`=? AND `user_id`=?'; const result: RunResult = db.run(deleteCharacterQuery, [characterId, 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 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 a character attribute from the database. * @param userId - The unique identifier of the user * @param attributeId - The unique identifier of the attribute to delete * @param lang - The language for error messages ('fr' or 'en') * @returns True if the deletion was successful, false otherwise */ static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr'): boolean { try { const db: Database = System.getDb(); const query: string = 'DELETE FROM `book_characters_attributes` WHERE `attr_id`=? AND `user_id`=?'; const params: SQLiteValue[] = [attributeId, 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 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 character. * @param characterId - The unique identifier of the character * @param userId - The unique identifier of the user * @param lang - The language for error messages ('fr' or 'en') * @returns An array of attribute results */ static fetchAttributes(characterId: string, userId: string, lang: 'fr' | 'en' = 'fr'): AttributeResult[] { try { const db: Database = System.getDb(); const query: string = 'SELECT attr_id, attribute_name, attribute_value FROM book_characters_attributes WHERE character_id=? AND user_id=?'; const params: SQLiteValue[] = [characterId, userId]; const attributes: AttributeResult[] = db.all(query, params) as AttributeResult[]; 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."); } } } /** * Fetches complete character information including attributes, optionally filtered by character IDs. * @param userId - The unique identifier of the user * @param bookId - The unique identifier of the book * @param tags - An optional array of character IDs to filter by * @param lang - The language for error messages ('fr' or 'en') * @returns An array of complete character results with attributes */ 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 params: SQLiteValue[] = [userId, bookId]; if (tags && tags.length > 0) { const placeholders: string = tags.map((): string => '?').join(','); query += ` AND charac.character_id IN (${placeholders})`; params.push(...tags); } const characters: CompleteCharacterResult[] = db.all(query, params) as CompleteCharacterResult[]; if (characters.length === 0) { throw new Error(lang === 'fr' ? `Aucun personnage complet trouvé.` : `No complete characters found.`); } 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 complets.` : `Unable to retrieve complete characters.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Updates an existing character attribute. * @param userId - The unique identifier of the user * @param characterAttributeId - The unique identifier of the attribute to update * @param attributeName - The new attribute name * @param attributeValue - The new attribute value * @param lastUpdate - The timestamp of the last update * @param lang - The language for error messages ('fr' or 'en') * @returns True if the update was successful, false otherwise */ 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 params: SQLiteValue[] = [attributeName, attributeValue, lastUpdate, characterAttributeId, 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 du personnage.` : `Unable to update character attribute.`); } 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 character exists in the database. * @param userId - The unique identifier of the user * @param characterId - The unique identifier of the character to check * @param lang - The language for error messages ('fr' or 'en') * @returns True if the character exists, false otherwise */ static isCharacterExist(userId: string, characterId: string, lang: "fr" | "en"): boolean { try { const db: Database = System.getDb(); const query: string = 'SELECT 1 FROM `book_characters` WHERE `character_id`=? AND `user_id`=?'; const params: SQLiteValue[] = [characterId, userId]; const character: QueryResult | null = db.get(query, params) || null; return character !== 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."); } } } /** * Checks if a character attribute exists in the database. * @param userId - The unique identifier of the user * @param characterAttributeId - The unique identifier of the attribute to check * @param lang - The language for error messages ('fr' or 'en') * @returns True if the attribute exists, false otherwise */ static isCharacterAttributeExist(userId: string, characterAttributeId: string, lang: "fr" | "en"): boolean { try { const db: Database = System.getDb(); const query: string = 'SELECT 1 FROM `book_characters_attributes` WHERE `attr_id`=? AND `user_id`=?'; const params: SQLiteValue[] = [characterAttributeId, userId]; const attribute: QueryResult | null = db.get(query, params) || null; return attribute !== 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 du personnage.` : `Unable to check character attribute 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 specific book asynchronously. * @param userId - The unique identifier of the user * @param bookId - The unique identifier of the book * @param lang - The language for error messages ('fr' or 'en') * @returns A promise resolving to an array of book characters */ static async fetchBookCharacters(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { try { const db: Database = System.getDb(); const query: string = 'SELECT character_id, book_id, user_id, first_name, last_name, category, title, image, role, biography, history, last_update FROM book_characters WHERE user_id=? AND book_id=?'; const params: SQLiteValue[] = [userId, bookId]; const characters: BookCharactersTable[] = db.all(query, params) as BookCharactersTable[]; 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.` : `Unable to retrieve characters.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Fetches all attributes for a specific character asynchronously. * @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 A promise resolving to an array of character attributes */ static async fetchBookCharactersAttributes(userId: string, characterId: string, lang: 'fr' | 'en'): Promise { try { const db: Database = System.getDb(); const query: string = 'SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM book_characters_attributes WHERE user_id=? AND character_id=?'; const params: SQLiteValue[] = [userId, characterId]; const attributes: BookCharactersAttributesTable[] = db.all(query, params) as BookCharactersAttributesTable[]; 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 des personnages.` : `Unable to retrieve character attributes.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Fetches all synced characters 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 synced character results */ static fetchSyncedCharacters(userId: string, lang: 'fr' | 'en'): SyncedCharacterResult[] { try { const db: Database = System.getDb(); const query: string = 'SELECT character_id, book_id, first_name, last_update FROM book_characters WHERE user_id = ?'; const params: SQLiteValue[] = [userId]; const syncedCharacters: SyncedCharacterResult[] = db.all(query, params) as SyncedCharacterResult[]; return syncedCharacters; } 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 synchronisés.` : `Unable to retrieve synced characters.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Fetches all synced character attributes 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 synced character attribute results */ static fetchSyncedCharacterAttributes(userId: string, lang: 'fr' | 'en'): SyncedCharacterAttributeResult[] { try { const db: Database = System.getDb(); const query: string = 'SELECT attr_id, character_id, attribute_name, last_update FROM book_characters_attributes WHERE user_id = ?'; const params: SQLiteValue[] = [userId]; const syncedAttributes: SyncedCharacterAttributeResult[] = db.all(query, params) as SyncedCharacterAttributeResult[]; return syncedAttributes; } 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 des personnages synchronisés.` : `Unable to retrieve synced character attributes.`); } else { console.error("An unknown error occurred."); throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Inserts a synced character into the database. * @param characterId - The unique identifier of the character * @param bookId - The unique identifier of the book * @param userId - The unique identifier of the user * @param firstName - The first name of the character * @param lastName - The last name of the character (nullable) * @param category - The category of the character * @param title - The title of the character (nullable) * @param image - The image path of the character (nullable) * @param role - The role of the character (nullable) * @param biography - The biography of the character (nullable) * @param history - The history of the character (nullable) * @param lastUpdate - The timestamp of the last update * @param lang - The language for error messages ('fr' or 'en') * @returns True if the insertion was successful, false otherwise */ static insertSyncCharacter(characterId: string, bookId: string, userId: string, firstName: string, lastName: string | null, category: string, title: string | null, image: string | null, role: string | null, biography: string | null, history: string | null, lastUpdate: number, lang: 'fr' | 'en'): boolean { try { const db: Database = System.getDb(); const query: string = `INSERT INTO book_characters (character_id, book_id, user_id, first_name, last_name, category, title, image, role, biography, history, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; const params: SQLiteValue[] = [characterId, bookId, userId, firstName, lastName, category, title, image, role, biography, history, 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.` : `Unable to insert character.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Inserts a synced character attribute into the database. * @param attrId - The unique identifier of the attribute * @param characterId - The unique identifier of the character * @param userId - The unique identifier of the user * @param attributeName - The name of the attribute * @param attributeValue - The value of the attribute * @param lastUpdate - The timestamp of the last update * @param lang - The language for error messages ('fr' or 'en') * @returns True if the insertion was successful, false otherwise */ static insertSyncCharacterAttribute(attrId: string, characterId: string, userId: string, attributeName: string, attributeValue: string, lastUpdate: number, lang: 'fr' | 'en'): boolean { 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 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 du personnage.` : `Unable to insert character attribute.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } /** * Fetches a complete character by its ID. * @param id - The unique identifier of the character * @param lang - The language for error messages ('fr' or 'en') * @returns A promise resolving to an array of book characters (typically one) */ static async fetchCompleteCharacterById(id: string, lang: "fr" | "en"): Promise { try { const db: Database = System.getDb(); const query: string = `SELECT character_id, book_id, user_id, first_name, last_name, category, title, image, role, biography, history, last_update FROM book_characters WHERE character_id = ?`; const params: SQLiteValue[] = [id]; const character: BookCharactersTable[] = db.all(query, params) as BookCharactersTable[]; return character; } catch (error: unknown) { if (error instanceof Error) { 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 its ID. * @param id - The unique identifier of the attribute * @param lang - The language for error messages ('fr' or 'en') * @returns A promise resolving to an array of character attributes (typically one) */ static async fetchCompleteCharacterAttributeById(id: string, lang: "fr" | "en"): Promise { try { const db: Database = System.getDb(); const query: string = `SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM book_characters_attributes WHERE attr_id = ?`; const params: SQLiteValue[] = [id]; const attribute: BookCharactersAttributesTable[] = db.all(query, params) as BookCharactersAttributesTable[]; return attribute; } catch (error: unknown) { if (error instanceof Error) { throw new Error(lang === 'fr' ? `Impossible de récupérer l'attribut de personnage complet.` : `Unable to retrieve complete character attribute.`); } else { throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); } } } }