From cf6fb97bf00683dc5c966ccd6339b90d9a59e404 Mon Sep 17 00:00:00 2001 From: natreex Date: Mon, 12 Jan 2026 13:38:10 -0500 Subject: [PATCH] Add models for guidelines, incidents, plot points, issues, acts, and world data - Introduced new models: `GuideLine`, `Incident`, `PlotPoint`, `Issue`, `Act`, and `World` for managing book-related entities. - Integrated encryption/decryption for sensitive properties in all models using user-specific keys. - Added methods for CRUD operations and synchronization workflows with error handling and multilingual support. - Improved maintainability with JSDoc comments and streamlined queries. --- electron/database/models/Act.ts | 185 ++ electron/database/models/Book.ts | 2257 ++--------------- electron/database/models/Chapter.ts | 601 +++-- electron/database/models/Character.ts | 282 +- electron/database/models/Content.ts | 99 +- electron/database/models/Cover.ts | 62 + electron/database/models/Download.ts | 200 ++ electron/database/models/EpubStyle.ts | 14 +- electron/database/models/GuideLine.ts | 216 ++ electron/database/models/Incident.ts | 105 + electron/database/models/Issue.ts | 98 + electron/database/models/Location.ts | 204 +- electron/database/models/Model.ts | 27 +- electron/database/models/PlotPoint.ts | 106 + electron/database/models/Sync.ts | 876 +++++++ electron/database/models/Upload.ts | 257 ++ electron/database/models/User.ts | 293 ++- electron/database/models/World.ts | 268 ++ .../repositories/incident.repository.ts | 2 +- 19 files changed, 3643 insertions(+), 2509 deletions(-) create mode 100644 electron/database/models/Act.ts create mode 100644 electron/database/models/Cover.ts create mode 100644 electron/database/models/Download.ts create mode 100644 electron/database/models/GuideLine.ts create mode 100644 electron/database/models/Incident.ts create mode 100644 electron/database/models/Issue.ts create mode 100644 electron/database/models/PlotPoint.ts create mode 100644 electron/database/models/Sync.ts create mode 100644 electron/database/models/Upload.ts create mode 100644 electron/database/models/World.ts diff --git a/electron/database/models/Act.ts b/electron/database/models/Act.ts new file mode 100644 index 0000000..dd9cdb6 --- /dev/null +++ b/electron/database/models/Act.ts @@ -0,0 +1,185 @@ +import { getUserEncryptionKey } from "../keyManager.js"; +import System from "../System.js"; +import PlotPoint, { PlotPointProps, PlotPointStory } from "./PlotPoint.js"; +import Incident, { IncidentProps, IncidentStory } from "./Incident.js"; +import ActRepository, { ActQuery } from "../repositories/act.repository.js"; +import Chapter, { ChapterProps } from "./Chapter.js"; +import IncidentRepository from "../repositories/incident.repository.js"; +import PlotPointRepository from "../repositories/plotpoint.repository.js"; +import ChapterRepo from "../repositories/chapter.repository.js"; + +export interface ActProps { + id: number; + summary: string | null; + incidents?: IncidentProps[]; + plotPoints?: PlotPointProps[]; + chapters?: ActChapter[]; +} + +export interface ActStory { + actId: number; + summary: string; + chapterSummary: string; + chapterGoal: string; + incidents: IncidentStory[]; + plotPoints: PlotPointStory[]; +} + +export interface ActChapter { + chapterInfoId: number; + chapterId: string; + title: string; + chapterOrder: number; + actId: number; + incidentId: string | null; + plotPointId: string | null; + summary: string; + goal: string; +} + +export interface SyncedActSummary { + id: string; + lastUpdate: number; +} + +export default class Act { + /** + * Retrieves all acts data for a specific book, including chapters, incidents, and plot points. + * Decrypts summaries using the user's encryption key. + * @param userId - The unique identifier of the user + * @param bookId - The unique identifier of the book + * @param lang - The language for localization ('fr' or 'en'), defaults to 'fr' + * @returns A promise resolving to an array of Act objects with their associated data + */ + public static async getActsData(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): Promise { + const userEncryptionKey: string = getUserEncryptionKey(userId); + const actChapters: ActChapter[] = Chapter.getAllChapterFromActs(userId, bookId, lang); + const actQueries: ActQuery[] = ActRepository.fetchAllActs(userId, bookId, lang); + const bookIncidents: IncidentProps[] = await Incident.getIncitentsIncidents(userId, bookId, actChapters); + const bookPlotPoints: PlotPointProps[] = await PlotPoint.getPlotPoints(userId, bookId, actChapters); + + const acts: ActProps[] = []; + + acts.push({ + id: 1, + summary: '', + chapters: actChapters.filter((chapter: ActChapter) => chapter.actId === 1) + }); + + acts.push({ + id: 2, + summary: '', + incidents: bookIncidents ? bookIncidents : [], + }); + + acts.push({ + id: 3, + summary: '', + plotPoints: bookPlotPoints ? bookPlotPoints : [], + }); + + acts.push({ + id: 4, + summary: '', + chapters: actChapters.filter((chapter: ActChapter) => chapter.actId === 4) + }); + + acts.push({ + id: 5, + summary: '', + chapters: actChapters.filter((chapter: ActChapter) => chapter.actId === 5) + }); + + if (actQueries.length > 0) { + for (const actQuery of actQueries) { + acts[actQuery.act_index - 1].summary = actQuery.summary && userEncryptionKey + ? System.decryptDataWithUserKey(actQuery.summary, userEncryptionKey) + : ''; + } + } + + return acts; + } + + /** + * Updates multiple acts including their summaries, incidents, plot points, and chapter information. + * Encrypts all sensitive data before storing in the database. + * @param acts - Array of act properties to update + * @param userId - The unique identifier of the user + * @param bookId - The unique identifier of the book + * @param userKey - The user's encryption key for data encryption + * @param lang - The language for localization ('fr' or 'en'), defaults to 'fr' + * @returns A promise resolving to true when all updates are complete + */ + public static async updateAct(acts: ActProps[], userId: string, bookId: string, userKey: string, lang: 'fr' | 'en' = 'fr'): Promise { + for (const act of acts) { + const actIncidents: IncidentProps[] = act.incidents ? act.incidents : []; + const actId: number = act.id; + + if (actId === 1 || actId === 4 || actId === 5) { + const encryptedActSummary: string = act.summary ? System.encryptDataWithUserKey(act.summary, userKey) : ''; + try { + ActRepository.updateActSummary(userId, bookId, actId, encryptedActSummary, System.timeStampInSeconds(), lang); + } catch (error: unknown) { + const newActSummaryId: string = System.createUniqueId(); + ActRepository.insertActSummary(newActSummaryId, userId, bookId, actId, encryptedActSummary, lang); + } + if (act.chapters) { + Chapter.updateChapterInfos(act.chapters, userId, actId, bookId, null, null, lang); + } + } else if (actId === 2) { + for (const incident of actIncidents) { + const encryptedIncidentSummary: string = incident.summary ? System.encryptDataWithUserKey(incident.summary, userKey) : ''; + const incidentId: string = incident.incidentId; + const incidentTitle: string = incident.title; + const hashedIncidentTitle: string = System.hashElement(incidentTitle); + const encryptedIncidentTitle: string = System.encryptDataWithUserKey(incidentTitle, userKey); + IncidentRepository.updateIncident(userId, bookId, incidentId, encryptedIncidentTitle, hashedIncidentTitle, encryptedIncidentSummary, System.timeStampInSeconds(), lang); + if (incident.chapters) { + Chapter.updateChapterInfos(incident.chapters, userId, actId, bookId, incidentId, null, lang); + } + } + } else { + const actPlotPoints: PlotPointProps[] = act.plotPoints ? act.plotPoints : []; + for (const plotPoint of actPlotPoints) { + const encryptedPlotPointSummary: string = plotPoint.summary ? System.encryptDataWithUserKey(plotPoint.summary, userKey) : ''; + const plotPointId: string = plotPoint.plotPointId; + const plotPointTitle: string = plotPoint.title; + const hashedPlotPointTitle: string = System.hashElement(plotPointTitle); + const encryptedPlotPointTitle: string = System.encryptDataWithUserKey(plotPointTitle, userKey); + PlotPointRepository.updatePlotPoint(userId, bookId, plotPointId, encryptedPlotPointTitle, hashedPlotPointTitle, encryptedPlotPointSummary, System.timeStampInSeconds(), lang); + if (plotPoint.chapters) { + Chapter.updateChapterInfos(plotPoint.chapters, userId, actId, bookId, null, plotPointId, lang); + } + } + } + } + return true; + } + + /** + * Updates the story structure including acts and main chapters. + * Encrypts chapter titles and updates their order in the database. + * @param userId - The unique identifier of the user + * @param bookId - The unique identifier of the book + * @param acts - Array of act properties to update + * @param mainChapters - Array of main chapter properties to update + * @param lang - The language for localization ('fr' or 'en'), defaults to 'fr' + * @returns True when all updates are complete + */ + public static updateStory(userId: string, bookId: string, acts: ActProps[], mainChapters: ChapterProps[], lang: 'fr' | 'en' = 'fr'): boolean { + const userEncryptionKey: string = getUserEncryptionKey(userId); + Act.updateAct(acts, userId, bookId, userEncryptionKey, lang).then(); + + for (const chapter of mainChapters) { + const chapterId: string = chapter.chapterId; + const chapterTitle: string = chapter.title; + const hashedChapterTitle: string = System.hashElement(chapterTitle); + const encryptedChapterTitle: string = System.encryptDataWithUserKey(chapterTitle, userEncryptionKey); + const chapterOrder: number = chapter.chapterOrder; + ChapterRepo.updateChapter(userId, chapterId, encryptedChapterTitle, hashedChapterTitle, chapterOrder, System.timeStampInSeconds(), lang); + } + + return true; + } +} diff --git a/electron/database/models/Book.ts b/electron/database/models/Book.ts index 28dffd5..d8e2343 100644 --- a/electron/database/models/Book.ts +++ b/electron/database/models/Book.ts @@ -1,64 +1,52 @@ -import type { - ActQuery, - BookActSummariesTable, BookAIGuideLineTable, BookChapterContentTable, BookChapterInfosTable, BookChaptersTable, - BookCharactersAttributesTable, - BookCharactersTable, BookGuideLineTable, BookIncidentsTable, BookIssuesTable, BookLocationTable, - BookPlotPointsTable, BookWorldElementsTable, BookWorldTable, - EritBooksTable, - IncidentQuery, - IssueQuery, LocationElementTable, LocationSubElementTable, - PlotPointQuery, - SyncedActSummaryResult, - SyncedAIGuideLineResult, - SyncedBookResult, - SyncedChapterContentResult, - SyncedChapterInfoResult, - SyncedChapterResult, - SyncedCharacterAttributeResult, - SyncedCharacterResult, - SyncedGuideLineResult, - SyncedIncidentResult, - SyncedIssueResult, - SyncedLocationElementResult, - SyncedLocationResult, - SyncedLocationSubElementResult, - SyncedPlotPointResult, - SyncedWorldElementResult, - SyncedWorldResult, - WorldQuery -} from '../repositories/book.repository.js'; -import BookRepository from '../repositories/book.repository.js'; -import BookRepo, { - BookCoverQuery, - BookQuery, - ChapterBookResult, - GuideLineAIQuery, - GuideLineQuery, - WorldElementValue -} from '../repositories/book.repository.js'; import System from '../System.js'; -import {getUserEncryptionKey} from '../keyManager.js'; -import path from "path"; -import fs from "fs"; -import Chapter, {ActChapter, ChapterContentData, ChapterProps} from "./Chapter.js"; +import { getUserEncryptionKey } from '../keyManager.js'; +import BookRepo, { BookQuery, EritBooksTable } from "../repositories/book.repository.js"; +import { BookActSummariesTable } from "../repositories/act.repository.js"; +import { BookAIGuideLineTable, BookGuideLineTable } from "../repositories/guideline.repository.js"; +import ChapterRepo, { + BookChapterInfosTable, + BookChaptersTable, + ChapterBookResult +} from "../repositories/chapter.repository.js"; +import { BookChapterContentTable } from "../repositories/chaptercontent.repository.js"; +import { + BookCharactersAttributesTable, + BookCharactersTable +} from "../repositories/character.repository.js"; +import { BookIncidentsTable } from "../repositories/incident.repository.js"; +import { BookIssuesTable } from "../repositories/issue.repository.js"; +import { + BookLocationTable, + LocationElementTable, + LocationSubElementTable +} from "../repositories/location.repository.js"; +import { BookPlotPointsTable } from "../repositories/plotpoint.repository.js"; +import { BookWorldElementsTable, BookWorldTable } from "../repositories/world.repository.js"; +import { CompleteChapterContent, SyncedChapter } from "./Chapter.js"; +import { SyncedCharacter } from "./Character.js"; +import { SyncedLocation } from "./Location.js"; +import { SyncedWorld } from "./World.js"; +import { SyncedIncident } from "./Incident.js"; +import { SyncedPlotPoint } from "./PlotPoint.js"; +import { SyncedIssue } from "./Issue.js"; +import { SyncedActSummary } from "./Act.js"; +import { SyncedAIGuideLine, SyncedGuideLine } from "./GuideLine.js"; +import Cover from "./Cover.js"; import UserRepo from "../repositories/user.repository.js"; -import ChapterRepo from "../repositories/chapter.repository.js"; -import CharacterRepo from "../repositories/character.repository.js"; -import LocationRepo from "../repositories/location.repository.js"; -export interface BookProps{ - id:string; - type:string; - authorId:string; - title:string; - subTitle?:string; - summary?:string; - serieId?:number; - desiredReleaseDate?:string; - desiredWordCount?:number; - wordCount?:number; - coverImage?:string; - bookMeta?:string; +export interface BookProps { + id: string; + type: string; + authorId: string; + title: string; + subTitle?: string; + summary?: string; + serieId?: number; + desiredReleaseDate?: string; + desiredWordCount?: number; + wordCount?: number; + coverImage?: string; + bookMeta?: string; } export interface CompleteBook { @@ -119,187 +107,6 @@ export interface BookSyncCompare { aiGuideLine: boolean; } -export interface SyncedChapter { - id: string; - name: string; - lastUpdate: number; - contents: SyncedChapterContent[]; - info: SyncedChapterInfo | null; -} - -export interface SyncedChapterContent { - id: string; - lastUpdate: number; -} - -export interface SyncedChapterInfo { - id: string; - lastUpdate: number; -} - -export interface SyncedCharacter { - id: string; - name: string; - lastUpdate: number; - attributes: SyncedCharacterAttribute[]; -} - -export interface SyncedCharacterAttribute { - id: string; - name: string; - lastUpdate: number; -} - -export interface SyncedLocation { - id: string; - name: string; - lastUpdate: number; - elements: SyncedLocationElement[]; -} - -export interface SyncedLocationElement { - id: string; - name: string; - lastUpdate: number; - subElements: SyncedLocationSubElement[]; -} - -export interface SyncedLocationSubElement { - id: string; - name: string; - lastUpdate: number; -} - -export interface SyncedWorld { - id: string; - name: string; - lastUpdate: number; - elements: SyncedWorldElement[]; -} - -export interface SyncedWorldElement { - id: string; - name: string; - lastUpdate: number; -} - -export interface SyncedIncident { - id: string; - name: string; - lastUpdate: number; -} - -export interface SyncedPlotPoint { - id: string; - name: string; - lastUpdate: number; -} - -export interface SyncedIssue { - id: string; - name: string; - lastUpdate: number; -} - -export interface SyncedActSummary { - id: string; - lastUpdate: number; -} - -export interface SyncedGuideLine { - lastUpdate: number; -} - -export interface SyncedAIGuideLine { - lastUpdate: number; -} - -export interface GuideLine{ - tone:string; - atmosphere:string; - writingStyle:string; - themes:string; - symbolism: string; - motifs: string; - narrativeVoice: string; - pacing: string; - intendedAudience: string; - keyMessages: string; -} - -interface PlotPoint { - plotPointId: string, - title:string, - summary:string, - linkedIncidentId: string | null, - chapters?:ActChapter[] -} - -interface Incident { - incidentId: string, - title:string, - summary:string, - chapters?:ActChapter[] -} - -export interface ExportData { - buffer: Buffer; - fileName: string; -} - -export interface Act { - id: number; - summary: string | null; - incidents?:Incident[]; - plotPoints?:PlotPoint[], - chapters?:ActChapter[] -} - -export interface Issue { - id: string, - name: string -} - -export interface WorldElement { - id: string; - name: string; - description: string; - type?:number; -} - -export interface WorldProps { - id: string; - name: string; - history: string; - politics: string; - economy: string; - religion: string; - languages: string; - laws: WorldElement[]; - biomes: WorldElement[]; - issues: WorldElement[]; - customs: WorldElement[]; - kingdoms: WorldElement[]; - climate: WorldElement[]; - resources: WorldElement[]; - wildlife: WorldElement[]; - arts: WorldElement[]; - ethnicGroups: WorldElement[]; - socialClasses: WorldElement[]; - importantCharacters: WorldElement[]; -} - -export interface GuideLineAI { - narrativeType: number|null; - dialogueType: number|null; - globalResume: string|null; - atmosphere: string|null; - verbeTense: number|null; - langue: number|null; - currentResume: string|null; - themes: string|null; -} - export interface CompleteBookData { bookId: string; title: string; @@ -314,30 +121,27 @@ export interface CompleteBookData { chapters: CompleteChapterContent[]; } -export interface CompleteChapterContent { - id: string; - title: string; - content: string; - order: number; - version?: number; -} - export default class Book { private readonly id: string; - private type:string; + private type: string; private authorId: string; private title: string; private subTitle: string; private summary: string; private serieId: number; private desiredReleaseDate: string; - private desiredWordCount:number; + private desiredWordCount: number; private wordCount: number; private cover: string; - - constructor(id:string,authorId?:string) { + + /** + * Creates a new Book instance. + * @param id - The unique identifier of the book + * @param authorId - The unique identifier of the author (optional) + */ + constructor(id: string, authorId?: string) { this.id = id; - if (authorId){ + if (authorId) { this.authorId = authorId; } else { this.authorId = ''; @@ -352,6 +156,14 @@ export default class Book { this.cover = ''; this.type = ''; } + + /** + * Retrieves all books for a specific user. + * @param userId - The unique identifier of the user + * @param lang - The language for error messages ('fr' or 'en') + * @returns A promise resolving to an array of book properties + * @throws Error if the user encryption key is not found + */ public static async getBooks(userId: string, lang: 'fr' | 'en' = 'fr'): Promise { const userKey: string | null = getUserEncryptionKey(userId); if (!userKey) { @@ -360,13 +172,13 @@ export default class Book { ); } - const books:BookQuery[] = BookRepository.fetchBooks(userId, lang); + const books: BookQuery[] = BookRepo.fetchBooks(userId, lang); if (!books || books.length === 0) { return []; } - + return await Promise.all( - books.map(async (book: BookQuery):Promise => { + books.map(async (book: BookQuery): Promise => { return { id: book.book_id, type: book.type, @@ -378,63 +190,58 @@ export default class Book { desiredReleaseDate: book.desired_release_date || '', desiredWordCount: book.desired_word_count || 0, wordCount: book.words_count || 0, - coverImage: book.cover_image ? await this.getPicture(userId, userKey, book.cover_image) : '', + coverImage: book.cover_image ? Cover.getPicture(userId, userKey, book.cover_image) : '', }; }) ?? [] ); } - - public static async getCoverPicture(userId:string,bookId:string, lang: 'fr' | 'en' = 'fr'):Promise{ - const query:BookCoverQuery = BookRepo.fetchBookCover(userId,bookId,lang); - if (query){ - const userKey:string = getUserEncryptionKey(userId); - return System.decryptDataWithUserKey(query.cover_image,userKey); - } else { - return ''; - } - } - - public static async deleteCoverPicture(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): Promise { - const coverName:string = await Book.getCoverPicture(userId,bookId,lang); - return BookRepo.updateBookCover(bookId, '', userId, lang); - } - public static getPicture(userId: string, userKey: string, image: string, lang: 'fr' | 'en' = 'fr'): string { - if (!image) return ''; - try { - const decryptedFileName: string = System.decryptDataWithUserKey(image, userKey); - const userDirectory: string = path.join(''); - fs.accessSync(userDirectory, fs.constants.R_OK); - const data = fs.readFileSync(userDirectory); - return data.toString('base64'); - } catch (err) { - return ''; - } - } - - public static async addBook(bookId: string | null, userId: string, title: string, subTitle: string, summary: string, type: string, serie: number, publicationDate: string, desiredWordCount: number, lang: 'fr' | 'en' = 'fr'): Promise { - let id:string = ''; - const userKey:string|null = getUserEncryptionKey(userId); - const encryptedTitle:string = System.encryptDataWithUserKey(title, userKey); - const encryptedSubTitle:string = subTitle ? System.encryptDataWithUserKey(subTitle, userKey) : ''; - const encryptedSummary:string = summary ? System.encryptDataWithUserKey(summary, userKey) : ''; - const hashedTitle:string = System.hashElement(title); - const hashedSubTitle:string = subTitle ? System.hashElement(subTitle) : ''; - - if (BookRepo.verifyBookExist(hashedTitle,hashedSubTitle,userId,lang)){ + /** + * Adds a new book to the database. + * @param bookId - The unique identifier for the book (optional, will be generated if null) + * @param userId - The unique identifier of the user + * @param title - The title of the book + * @param subTitle - The subtitle of the book + * @param summary - The summary of the book + * @param type - The type/genre of the book + * @param serie - The series identifier + * @param publicationDate - The desired publication date + * @param desiredWordCount - The target word count + * @param lang - The language for error messages ('fr' or 'en') + * @returns A promise resolving to the book ID + * @throws Error if a book with the same title already exists + */ + public static async addBook(bookId: string | null, userId: string, title: string, subTitle: string, summary: string, type: string, serie: number, publicationDate: string, desiredWordCount: number, lang: 'fr' | 'en' = 'fr'): Promise { + let newBookId: string = ''; + const userKey: string | null = getUserEncryptionKey(userId); + + const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey); + const encryptedSubTitle: string = subTitle ? System.encryptDataWithUserKey(subTitle, userKey) : ''; + const encryptedSummary: string = summary ? System.encryptDataWithUserKey(summary, userKey) : ''; + const hashedTitle: string = System.hashElement(title); + const hashedSubTitle: string = subTitle ? System.hashElement(subTitle) : ''; + + if (BookRepo.verifyBookExist(hashedTitle, hashedSubTitle, userId, lang)) { throw new Error(lang === "fr" ? `Tu as déjà un livre intitulé ${title} - ${subTitle}.` : `You already have a book named ${title} - ${subTitle}.`); } - if (bookId){ - id = bookId; + if (bookId) { + newBookId = bookId; } else { - id = System.createUniqueId(); + newBookId = System.createUniqueId(); } - return BookRepo.insertBook(id,userId,encryptedTitle,hashedTitle,encryptedSubTitle,hashedSubTitle,encryptedSummary,type,serie,publicationDate,desiredWordCount,lang); + return BookRepo.insertBook(newBookId, userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, type, serie, publicationDate, desiredWordCount, lang); } - - public static async getBook(userId:string,bookId: string, lang: 'fr' | 'en'): Promise { - const book:Book = new Book(bookId); - await book.getBookInfos(userId); + + /** + * Retrieves a single book by its ID. + * @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 the book properties + */ + public static async getBook(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { + const book: Book = new Book(bookId); + book.getBookInfos(userId); return { id: book.getId(), type: book.getType(), @@ -449,578 +256,143 @@ export default class Book { coverImage: book.getCover() }; } - - public static async getGuideLine(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): Promise { - const guideLineResponse: GuideLineQuery[] = BookRepo.fetchGuideLine(userId, bookId,lang); - if (guideLineResponse.length === 0) { - return null; - } - const guideLine: GuideLineQuery = guideLineResponse[0]; - const userKey: string = getUserEncryptionKey(userId); - return { - tone: guideLine.tone ? System.decryptDataWithUserKey(guideLine.tone, userKey) : '', - atmosphere: guideLine.atmosphere ? System.decryptDataWithUserKey(guideLine.atmosphere, userKey) : '', - writingStyle: guideLine.writing_style ? System.decryptDataWithUserKey(guideLine.writing_style, userKey) : '', - themes: guideLine.themes ? System.decryptDataWithUserKey(guideLine.themes, userKey) : '', - symbolism: guideLine.symbolism ? System.decryptDataWithUserKey(guideLine.symbolism, userKey) : '', - motifs: guideLine.motifs ? System.decryptDataWithUserKey(guideLine.motifs, userKey) : '', - narrativeVoice: guideLine['narrative-voice'] ? System.decryptDataWithUserKey(guideLine['narrative-voice'] as string, userKey) : '', - pacing: guideLine.pacing ? System.decryptDataWithUserKey(guideLine.pacing, userKey) : '', - intendedAudience: guideLine.intended_audience ? System.decryptDataWithUserKey(guideLine.intended_audience, userKey) : '', - keyMessages: guideLine.key_messages ? System.decryptDataWithUserKey(guideLine.key_messages, userKey) : '', - }; - } - - public static async updateGuideLine(userId: string, bookId: string, tone: string | null, atmosphere: string | null, writingStyle: string | null, themes: string | null, symbolism: string | null, motifs: string | null, narrativeVoice: string | null, pacing: string | null, keyMessages: string | null, intendedAudience: string | null, lang: 'fr' | 'en' = 'fr'): Promise { - const userKey: string = getUserEncryptionKey(userId); - const encryptedTone: string = tone ? System.encryptDataWithUserKey(tone, userKey) : ''; - const encryptedAtmosphere: string = atmosphere ? System.encryptDataWithUserKey(atmosphere, userKey) : ''; - const encryptedWritingStyle: string = writingStyle ? System.encryptDataWithUserKey(writingStyle, userKey) : ''; - const encryptedThemes: string = themes ? System.encryptDataWithUserKey(themes, userKey) : ''; - const encryptedSymbolism: string = symbolism ? System.encryptDataWithUserKey(symbolism, userKey) : ''; - const encryptedMotifs: string = motifs ? System.encryptDataWithUserKey(motifs, userKey) : ''; - const encryptedNarrativeVoice: string = narrativeVoice ? System.encryptDataWithUserKey(narrativeVoice, userKey) : ''; - const encryptedPacing: string = pacing ? System.encryptDataWithUserKey(pacing, userKey) : ''; - const encryptedKeyMessages: string = keyMessages ? System.encryptDataWithUserKey(keyMessages, userKey) : ''; - const encryptedIntendedAudience: string = intendedAudience ? System.encryptDataWithUserKey(intendedAudience, userKey) : ''; - - return BookRepo.updateGuideLine(userId, bookId, encryptedTone, encryptedAtmosphere, encryptedWritingStyle, encryptedThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedKeyMessages, encryptedIntendedAudience, lang); - } - - public static addNewIncident(userId: string, bookId: string, name: string, lang: 'fr' | 'en' = 'fr', existingIncidentId?: string): string { - const userKey: string = getUserEncryptionKey(userId); - const encryptedName:string = System.encryptDataWithUserKey(name,userKey); - const hashedName:string = System.hashElement(name); - const incidentId: string = existingIncidentId || System.createUniqueId(); - return BookRepo.insertNewIncident(incidentId, userId, bookId, encryptedName, hashedName, lang); - } - public static async getPlotPoints(userId:string, bookId: string,actChapters:ActChapter[], lang: 'fr' | 'en' = 'fr'):Promise{ - const query:PlotPointQuery[] = BookRepo.fetchAllPlotPoints(userId,bookId,lang); - const userKey:string = getUserEncryptionKey(userId); - let plotPoints:PlotPoint[] = []; - if (query.length>0){ - for (const plot of query) { - let chapters:ActChapter[] = []; - for (const chapter of actChapters) { - if (chapter.plotPointId === plot.plot_point_id){ - chapters.push(chapter); - } - } - plotPoints.push({ - plotPointId: plot.plot_point_id, - title: plot.title ? System.decryptDataWithUserKey(plot.title,userKey) : '', - summary : plot.summary ? System.decryptDataWithUserKey(plot.summary,userKey) : '', - linkedIncidentId: plot.linked_incident_id, - chapters:chapters - }) - } - } - return plotPoints; - } - public static async getIncitentsIncidents(userId:string,bookId: string,actChapters:ActChapter[], lang: 'fr' | 'en' = 'fr'):Promise{ - const query:IncidentQuery[] = BookRepo.fetchAllIncitentIncidents(userId,bookId,lang); - let incidents:Incident[] = []; - const userKey:string = getUserEncryptionKey(userId); - if (query.length>0){ - for (const incident of query) { - let chapters:ActChapter[] = []; - for (const chapter of actChapters) { - if (chapter.incidentId === incident.incident_id){ - chapters.push(chapter); - } - } - incidents.push({ - incidentId: incident.incident_id, - title: incident.title ? System.decryptDataWithUserKey(incident.title,userKey) : '', - summary : incident.summary ? System.decryptDataWithUserKey(incident.summary,userKey) : '', - chapters:chapters - }) - } - } - return incidents; - } - - public static removeIncident(userId: string, bookId: string, incidentId: string, lang: 'fr' | 'en' = 'fr'): boolean { - return BookRepo.deleteIncident(userId, bookId, incidentId, lang); - } - - public static async getActsData(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): Promise { - const userKey:string = getUserEncryptionKey(userId); - const actChapters:ActChapter[] = Chapter.getAllChapterFromActs(userId, bookId, lang); - const query: ActQuery[] = BookRepo.fetchAllActs(userId, bookId, lang); - const incidents: Incident[] = await Book.getIncitentsIncidents(userId, bookId, actChapters); - const plotsPoint: PlotPoint[] = await Book.getPlotPoints(userId, bookId, actChapters); - let acts: Act[] = []; - acts.push({ - id: 1, - summary: '', - chapters: actChapters.filter((chapter: ActChapter) => chapter.actId === 1) - }) - acts.push({ - id: 2, - summary: '', - incidents: incidents ? incidents : [], - }) - acts.push({ - id: 3, - summary: '', - plotPoints: plotsPoint ? plotsPoint : [], - }) - acts.push({ - id: 4, - summary: '', - chapters: actChapters.filter((chapter: ActChapter) => chapter.actId === 4) - }) - acts.push({ - id: 5, - summary: '', - chapters: actChapters.filter((chapter: ActChapter) => chapter.actId === 5) - }) - if (query.length > 0) { - for (const act of query) { - acts[act.act_index - 1].summary = act.summary && userKey ? System.decryptDataWithUserKey(act.summary, userKey) : ''; - } - } - return acts; - } - public static async getIssuesFromBook(userId:string,bookId:string, lang: 'fr' | 'en' = 'fr'):Promise{ - const query:IssueQuery[] = BookRepo.fetchIssuesFromBook(userId,bookId,lang); - const userKey:string = getUserEncryptionKey(userId); - let issues:Issue[] = []; - if (query.length>0){ - for (const issue of query) { - issues.push({ - id:issue.issue_id, - name: System.decryptDataWithUserKey(issue.name,userKey) - }) - } - } - return issues; - } - + + /** + * Updates basic information for a book. + * @param userId - The unique identifier of the user + * @param title - The new title + * @param subTitle - The new subtitle + * @param summary - The new summary + * @param publicationDate - The new publication date + * @param wordCount - The new word count + * @param bookId - The unique identifier of the book + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the update was successful, false otherwise + */ static updateBookBasicInformation(userId: string, title: string, subTitle: string, summary: string, publicationDate: string, wordCount: number, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean { - const userKey:string = getUserEncryptionKey(userId); - const encryptedTitle:string = System.encryptDataWithUserKey(title, userKey); - const encryptedSubTitle:string = subTitle ? System.encryptDataWithUserKey(subTitle, userKey) : ''; - const encryptedSummary:string = summary ? System.encryptDataWithUserKey(summary, userKey) : ''; - const hashedTitle:string = System.hashElement(title); - const hashedSubTitle:string = subTitle ? System.hashElement(subTitle) : ''; + const userKey: string = getUserEncryptionKey(userId); + const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey); + const encryptedSubTitle: string = subTitle ? System.encryptDataWithUserKey(subTitle, userKey) : ''; + const encryptedSummary: string = summary ? System.encryptDataWithUserKey(summary, userKey) : ''; + const hashedTitle: string = System.hashElement(title); + const hashedSubTitle: string = subTitle ? System.hashElement(subTitle) : ''; return BookRepo.updateBookBasicInformation(userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, publicationDate, wordCount, bookId, lang); } - - static addNewPlotPoint(userId: string, bookId: string, incidentId: string, name: string, lang: 'fr' | 'en' = 'fr', existingPlotPointId?: string): string { - const userKey:string = getUserEncryptionKey(userId); - const encryptedName:string = System.encryptDataWithUserKey(name, userKey); - const hashedName:string = System.hashElement(name); - const plotPointId: string = existingPlotPointId || System.createUniqueId(); - return BookRepo.insertNewPlotPoint(plotPointId, userId, bookId, encryptedName, hashedName, incidentId, lang); - } - - static removePlotPoint(userId: string, plotId: string, lang: 'fr' | 'en' = 'fr'): boolean{ - return BookRepo.deletePlotPoint(userId, plotId, lang); - } - - public static addNewIssue(userId: string, bookId: string, name: string, lang: 'fr' | 'en' = 'fr', existingIssueId?: string): string { - const userKey:string = getUserEncryptionKey(userId); - const encryptedName:string = System.encryptDataWithUserKey(name,userKey); - const hashedName:string = System.hashElement(name); - const issueId: string = existingIssueId || System.createUniqueId(); - return BookRepo.insertNewIssue(issueId, userId, bookId, encryptedName, hashedName,lang); - } - - public static removeIssue(userId: string, issueId: string, lang: 'fr' | 'en' = 'fr'): boolean{ - return BookRepo.deleteIssue(userId, issueId,lang); - } - - public static async updateAct(acts: Act[], userId: string, bookId: string, userKey: string, lang: 'fr' | 'en' = 'fr'): Promise { - for (const act of acts) { - const incidents: Incident[] = act.incidents ? act.incidents : []; - const actId: number = act.id; - if (actId === 1 || actId === 4 || actId === 5) { - const actSummary: string = act.summary ? System.encryptDataWithUserKey(act.summary, userKey) : ''; - try { - BookRepo.updateActSummary(userId, bookId, actId, actSummary,System.timeStampInSeconds(),lang); - } catch (e: unknown) { - const actSummaryId: string = System.createUniqueId(); - BookRepo.insertActSummary(actSummaryId, userId, bookId, actId, actSummary,lang); - } - if (act.chapters) { - Chapter.updateChapterInfos(act.chapters, userId, actId, bookId, null, null, lang); - } - } else if (actId === 2) { - for (const incident of incidents) { - const incidentSummary: string = incident.summary ? System.encryptDataWithUserKey(incident.summary, userKey) : ''; - const incidentId: string = incident.incidentId; - const incidentName: string = incident.title; - const incidentHashedName: string = System.hashElement(incidentName); - const encryptedIncidentName: string = System.encryptDataWithUserKey(incidentName, userKey); - BookRepo.updateIncident(userId, bookId, incidentId, encryptedIncidentName, incidentHashedName, incidentSummary, System.timeStampInSeconds(), lang); - if (incident.chapters) { - Chapter.updateChapterInfos(incident.chapters, userId, actId, bookId, incidentId, null, lang); - } - } - } else { - const plotPoints: PlotPoint[] = act.plotPoints ? act.plotPoints : []; - for (const plotPoint of plotPoints) { - const plotPointSummary: string = plotPoint.summary ? System.encryptDataWithUserKey(plotPoint.summary, userKey) : ''; - const plotPointId: string = plotPoint.plotPointId; - const plotPointName: string = plotPoint.title; - const plotPointHashedName: string = System.hashElement(plotPointName); - const encryptedPlotPointName: string = System.encryptDataWithUserKey(plotPointName, userKey); - BookRepo.updatePlotPoint(userId, bookId, plotPointId, encryptedPlotPointName, plotPointHashedName, plotPointSummary, System.timeStampInSeconds(), lang); - if (plotPoint.chapters) { - Chapter.updateChapterInfos(plotPoint.chapters, userId, actId, bookId, null, plotPointId, lang); - } - } - } - } - return true; - } - - public static updateStory(userId: string, bookId: string, acts: Act[], mainChapters: ChapterProps[], lang: 'fr' | 'en' = 'fr'): boolean { - const userKey: string = getUserEncryptionKey(userId); - Book.updateAct(acts, userId, bookId, userKey, lang); - for (const chapter of mainChapters) { - const chapterId: string = chapter.chapterId; - const chapterTitle: string = chapter.title; - const chapterHashedTitle: string = System.hashElement(chapterTitle); - const encryptedTitle: string = System.encryptDataWithUserKey(chapterTitle, userKey); - const chapterOrder: number = chapter.chapterOrder; - ChapterRepo.updateChapter(userId, chapterId, encryptedTitle, chapterHashedTitle, chapterOrder, System.timeStampInSeconds(), lang); - } - return true; - } - - public static addNewWorld(userId: string, bookId: string, worldName: string, lang: 'fr' | 'en' = 'fr', existingWorldId?: string): string { - const userKey: string = getUserEncryptionKey(userId); - const hashedName: string = System.hashElement(worldName); - if (!existingWorldId && BookRepo.checkWorldExist(userId, bookId, hashedName, lang)) { - throw new Error(lang === "fr" ? `Tu as déjà un monde ${worldName}.` : `You already have a world named ${worldName}.`); - } - const encryptedName: string = System.encryptDataWithUserKey(worldName, userKey); - const worldId: string = existingWorldId || System.createUniqueId(); - return BookRepo.insertNewWorld(worldId, userId, bookId, encryptedName, hashedName, lang); - } - - public static getWorlds(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): WorldProps[] { - const worldElements: WorldQuery[] = BookRepo.fetchWorlds(userId, bookId, lang); - const userKey: string = getUserEncryptionKey(userId); - let worlds: WorldProps[] = [] - for (const element of worldElements) { - const existWorld: WorldProps | undefined = worlds.find((world: WorldProps) => world.id === element.world_id); - - if (!existWorld) { - const newWorld: WorldProps = { - id: element.world_id, - name: System.decryptDataWithUserKey(element.world_name, userKey), - history: element.history ? System.decryptDataWithUserKey(element.history, userKey) : '', - politics: element.politics ? System.decryptDataWithUserKey(element.politics, userKey) : '', - economy: element.economy ? System.decryptDataWithUserKey(element.economy, userKey) : '', - religion: element.religion ? System.decryptDataWithUserKey(element.religion, userKey) : '', - languages: element.languages ? System.decryptDataWithUserKey(element.languages, userKey) : '', - laws: [], - biomes: [], - issues: [], - customs: [], - kingdoms: [], - climate: [], - resources: [], - wildlife: [], - arts: [], - ethnicGroups: [], - socialClasses: [], - importantCharacters: [], - }; - - worlds.push(newWorld); - - if (element.element_type) { - const newElement = { - id: element.element_id as string, - name: element.element_name ? System.decryptDataWithUserKey(element.element_name, userKey) : '', - description: element.element_description ? System.decryptDataWithUserKey(element.element_description, userKey) : '' - }; - - switch (element.element_type) { - case 1: - worlds[worlds.length - 1].laws.push(newElement); - break; - case 2: - worlds[worlds.length - 1].biomes.push(newElement); - break; - case 3: - worlds[worlds.length - 1].issues.push(newElement); - break; - case 4: - worlds[worlds.length - 1].customs.push(newElement); - break; - case 5: - worlds[worlds.length - 1].kingdoms.push(newElement); - break; - case 6: - worlds[worlds.length - 1].climate.push(newElement); - break; - case 7: - worlds[worlds.length - 1].resources.push(newElement); - break; - case 8: - worlds[worlds.length - 1].wildlife.push(newElement); - break; - case 9: - worlds[worlds.length - 1].arts.push(newElement); - break; - case 10: - worlds[worlds.length - 1].ethnicGroups.push(newElement); - break; - case 11: - worlds[worlds.length - 1].socialClasses.push(newElement); - break; - case 12: - worlds[worlds.length - 1].importantCharacters.push(newElement); - break; - } - } - } else { - const existingElement = { - id: element.element_id as string, - name: element.element_name ? System.decryptDataWithUserKey(element.element_name, userKey) : '', - description: element.element_description ? System.decryptDataWithUserKey(element.element_description, userKey) : '' - }; - - switch (element.element_type) { - case 1: - existWorld.laws.push(existingElement); - break; - case 2: - existWorld.biomes.push(existingElement); - break; - case 3: - existWorld.issues.push(existingElement); - break; - case 4: - existWorld.customs.push(existingElement); - break; - case 5: - existWorld.kingdoms.push(existingElement); - break; - case 6: - existWorld.climate.push(existingElement); - break; - case 7: - existWorld.resources.push(existingElement); - break; - case 8: - existWorld.wildlife.push(existingElement); - break; - case 9: - existWorld.arts.push(existingElement); - break; - case 10: - existWorld.ethnicGroups.push(existingElement); - break; - case 11: - existWorld.socialClasses.push(existingElement); - break; - case 12: - existWorld.importantCharacters.push(existingElement); - break; - } - } - } - return worlds; - } - - public static updateWorld(userId: string, world: WorldProps, lang: 'fr' | 'en' = 'fr'): boolean { - const userKey: string = getUserEncryptionKey(userId); - const encryptName: string = world.name ? System.encryptDataWithUserKey(world.name, userKey) : ''; - const encryptHistory: string = world.history ? System.encryptDataWithUserKey(world.history, userKey) : ''; - const encryptPolitics: string = world.politics ? System.encryptDataWithUserKey(world.politics, userKey) : ''; - const encryptEconomy: string = world.economy ? System.encryptDataWithUserKey(world.economy, userKey) : ''; - const encryptReligion: string = world.religion ? System.encryptDataWithUserKey(world.religion, userKey) : ''; - const encryptLanguages: string = world.languages ? System.encryptDataWithUserKey(world.languages, userKey) : ''; - let elements: WorldElementValue[] = []; - const elementTypes: { key: keyof WorldProps, elements: WorldElement[] }[] = [ - {key: 'laws', elements: world.laws}, - {key: 'biomes', elements: world.biomes}, - {key: 'issues', elements: world.issues}, - {key: 'customs', elements: world.customs}, - {key: 'kingdoms', elements: world.kingdoms}, - {key: 'climate', elements: world.climate}, - {key: 'resources', elements: world.resources}, - {key: 'wildlife', elements: world.wildlife}, - {key: 'arts', elements: world.arts}, - {key: 'ethnicGroups', elements: world.ethnicGroups}, - {key: 'socialClasses', elements: world.socialClasses}, - {key: 'importantCharacters', elements: world.importantCharacters} - ]; - - elementTypes.forEach(({key, elements: elementsList}) => { - elements = elements.concat(elementsList.map((element: WorldElement) => { - const encryptedName: string = System.encryptDataWithUserKey(element.name, userKey); - const hashedName: string = System.hashElement(element.name); - const encryptedDescription: string = element.description ? System.encryptDataWithUserKey(element.description, userKey) : ''; - const elementType: number = Book.getElementTypes(key); - - return { - id: element.id, - name: encryptedName, - hashedName: hashedName, - description: encryptedDescription, - type: elementType - }; - })); - }); - - BookRepo.updateWorld(userId, world.id, encryptName, System.hashElement(world.name), encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, System.timeStampInSeconds(), lang); - return BookRepo.updateWorldElements(userId, elements, lang); - } - - public static addNewElementToWorld(userId: string, worldId: string, elementName: string, elementType: string, lang: 'fr' | 'en' = 'fr', existingElementId?: string): string { - const userKey: string = getUserEncryptionKey(userId); - const hashedName: string = System.hashElement(elementName); - if (!existingElementId && BookRepo.checkElementExist(worldId, hashedName, lang)) { - throw new Error(lang === "fr" ? `Vous avez déjà un élément avec ce nom ${elementName}.` : `You already have an element named ${elementName}.`); - } - const elementTypeId: number = Book.getElementTypes(elementType); - const encryptedName: string = System.encryptDataWithUserKey(elementName, userKey); - const elementId: string = existingElementId || System.createUniqueId(); - return BookRepo.insertNewElement(userId, elementId, elementTypeId, worldId, encryptedName, hashedName, lang); - } - public static getElementTypes(elementType:string):number{ - switch (elementType){ - case 'laws': - return 1; - case 'biomes': - return 2; - case 'issues': - return 3; - case 'customs': - return 4; - case 'kingdoms': - return 5; - case 'climate': - return 6; - case 'resources': - return 7; - case 'wildlife': - return 8; - case 'arts': - return 9; - case 'ethnicGroups': - return 10; - case 'socialClasses': - return 11; - case 'importantCharacters': - return 12; - default: - return 0; - } - } - - public static removeElementFromWorld(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { - return BookRepo.deleteElement(userId, elementId, lang); - } + /** + * Removes a book from the database. + * @param userId - The unique identifier of the user + * @param bookId - The unique identifier of the book to remove + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the book was removed, false otherwise + */ public static removeBook(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean { return BookRepo.deleteBook(userId, bookId, lang); } - static getGuideLineAI(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): GuideLineAI { - const userKey: string = getUserEncryptionKey(userId); - try { - const guideLine: GuideLineAIQuery = BookRepo.fetchGuideLineAI(userId, bookId, lang); - return { - narrativeType: guideLine.narrative_type, - dialogueType: guideLine.dialogue_type, - globalResume: guideLine.global_resume ? System.decryptDataWithUserKey(guideLine.global_resume, userKey) : '', - atmosphere: guideLine.atmosphere ? System.decryptDataWithUserKey(guideLine.atmosphere, userKey) : '', - verbeTense: guideLine.verbe_tense, - themes: guideLine.themes ? System.decryptDataWithUserKey(guideLine.themes, userKey) : '', - currentResume: guideLine.current_resume ? System.decryptDataWithUserKey(guideLine.current_resume, userKey) : '', - langue: guideLine.langue - } - } catch (e: unknown) { - if (e instanceof Error && e.message.includes('not found')) { - return { - narrativeType: 0, - dialogueType: 0, - globalResume: '', - atmosphere: '', - verbeTense: 0, - themes: '', - currentResume: '', - langue: 0 - } - } - if (e instanceof Error) { - throw new Error(e.message); - } else { - console.error(lang === 'fr' ? "Erreur inconnue lors de la récupération de la ligne directrice de l'IA." : "Unknown error while fetching AI guideline."); - throw new Error(lang === 'fr' ? "Erreur inconnue lors de la récupération de la ligne directrice de l'IA." : "Unknown error while fetching AI guideline."); - } - } - } - - public static setAIGuideLine(userId: string, bookId: string, narrativeType: number, dialogueType: number, plotSummary: string, toneAtmosphere: string, verbTense: number, language: number, themes: string, lang: 'fr' | 'en' = 'fr'): boolean { - const userKey: string = getUserEncryptionKey(userId); - const encryptedPlotSummary: string = plotSummary ? System.encryptDataWithUserKey(plotSummary, userKey) : ''; - const encryptedToneAtmosphere: string = toneAtmosphere ? System.encryptDataWithUserKey(toneAtmosphere, userKey) : ''; - const encryptedThemes: string = themes ? System.encryptDataWithUserKey(themes, userKey) : ''; - return BookRepo.insertAIGuideLine(userId, bookId, narrativeType, dialogueType, encryptedPlotSummary, encryptedToneAtmosphere, verbTense, language, encryptedThemes, lang); - } - + /** + * Gets the book ID. + * @returns The book's unique identifier + */ public getId(): string { return this.id; } - + + /** + * Gets the author ID. + * @returns The author's unique identifier + */ public getAuthorId(): string { return this.authorId; } - + + /** + * Gets the book title. + * @returns The decrypted book title + */ public getTitle(): string { return this.title; } - + + /** + * Gets the book subtitle. + * @returns The decrypted book subtitle + */ public getSubTitle(): string { return this.subTitle; } - + + /** + * Gets the book summary. + * @returns The decrypted book summary + */ public getSummary(): string { return this.summary; } - + + /** + * Gets the series ID. + * @returns The series identifier + */ public getSerieId(): number { return this.serieId; } - + + /** + * Gets the desired release date. + * @returns The desired release date string + */ public getDesiredReleaseDate(): string { return this.desiredReleaseDate; } - + + /** + * Gets the desired word count. + * @returns The target word count + */ public getDesiredWordCount(): number { return this.desiredWordCount; } - + + /** + * Gets the current word count. + * @returns The current word count + */ public getWordCount(): number { return this.wordCount; } - + + /** + * Gets the cover image. + * @returns The cover image data + */ public getCover(): string { return this.cover; } - + + /** + * Gets the book type. + * @returns The book type/genre + */ public getType(): string { return this.type; } - + + /** + * Retrieves complete book data including chapters and user information. + * @param userId - The unique identifier of the user + * @param id - The unique identifier of the book + * @param lang - The language for error messages ('fr' or 'en') + * @returns The complete book data with decrypted content + */ static completeBookData(userId: string, id: string, lang: 'fr' | 'en' = 'fr'): CompleteBookData { - const data = BookRepo.fetchBook(id, userId, lang); - const chapters: ChapterBookResult[] = BookRepo.fetchCompleteBookChapters(id, lang); + const bookData: BookQuery = BookRepo.fetchBook(id, userId, lang); + const chapters: ChapterBookResult[] = ChapterRepo.fetchCompleteBookChapters(id, lang); const userKey: string = getUserEncryptionKey(userId); const userInfos = UserRepo.fetchAccountInformation(userId, lang); - const bookTitle: string = data.title ? System.decryptDataWithUserKey(data.title, userKey) : ''; - const decryptedChapters: any[] = []; + const bookTitle: string = bookData.title ? System.decryptDataWithUserKey(bookData.title, userKey) : ''; + const decryptedChapters: CompleteChapterContent[] = []; for (const chapter of chapters) { decryptedChapters.push({ @@ -1030,12 +402,12 @@ export default class Book { order: chapter.chapter_order }) } - const coverImage: string = data.cover_image ? Book.getPicture(userId, userKey, data.cover_image, lang) : ''; + const coverImage: string = bookData.cover_image ? Cover.getPicture(userId, userKey, bookData.cover_image, lang) : ''; return { bookId: id, title: bookTitle, - subTitle: data.sub_title ? System.decryptDataWithUserKey(data.sub_title, userKey) : '', - summary: data.summary ? System.decryptDataWithUserKey(data.summary, userKey) : '', + subTitle: bookData.sub_title ? System.decryptDataWithUserKey(bookData.sub_title, userKey) : '', + summary: bookData.summary ? System.decryptDataWithUserKey(bookData.summary, userKey) : '', coverImage: coverImage, userInfos: { firstName: userInfos.first_name ? System.decryptDataWithUserKey(userInfos.first_name, userKey) : '', @@ -1045,57 +417,26 @@ export default class Book { chapters: decryptedChapters }; } - - static getChaptersOrSheet(bookChapters: CompleteChapterContent[]): ChapterContentData[] { - const chapters: ChapterContentData[] = []; - const haveSheet: CompleteChapterContent | undefined = bookChapters.find((chapter: CompleteChapterContent): boolean => chapter.order === -1); - const haveChapter: CompleteChapterContent | undefined = bookChapters.find((chapter: CompleteChapterContent): boolean => chapter.order > 0); - if (haveSheet && !haveChapter) { - chapters.push({ - title: haveSheet.title, - chapterOrder: haveSheet.order, - content: System.htmlToText(Chapter.tipTapToHtml(JSON.parse(haveSheet.content))), - wordsCount: 0, - version: haveSheet.version || 0 - }); - } else if (haveChapter) { - for (const chapter of bookChapters) { - if (chapter.order < 0) continue; - chapters.push({ - title: chapter.title, - chapterOrder: chapter.order, - content: System.htmlToText(Chapter.tipTapToHtml(JSON.parse(chapter.content))), - wordsCount: 0, - version: chapter.version || 0 - }); - } - } - return chapters; - } - static getAllChapters(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterContentData[] { - try { - const book: CompleteBookData = Book.completeBookData(userId, bookId, lang); - return Book.getChaptersOrSheet(book.chapters); - } catch (e: unknown) { - return []; - } - } - + /** + * Loads book information from the database into the instance. + * @param userId - The unique identifier of the user + * @param lang - The language for error messages ('fr' or 'en') + */ public getBookInfos(userId: string, lang: 'fr' | 'en' = 'fr'): void { - const book: BookQuery = BookRepo.fetchBook(this.id, userId, lang); + const bookData: BookQuery = BookRepo.fetchBook(this.id, userId, lang); const userKey: string = getUserEncryptionKey(userId); - if (book) { - this.authorId = book.author_id; - this.type = book.type; - this.title = book.title ? System.decryptDataWithUserKey(book.title, userKey) : ''; - this.subTitle = book.sub_title ? System.decryptDataWithUserKey(book.sub_title, userKey) : ''; - this.summary = book.summary ? System.decryptDataWithUserKey(book.summary, userKey) : ''; - this.serieId = book.serie_id ?? 0; - this.desiredReleaseDate = book.desired_release_date ?? ''; - this.desiredWordCount = book.desired_word_count ?? 0; - this.wordCount = book.words_count ?? 0; - this.cover = book.cover_image ? Book.getPicture(userId, userKey, book.cover_image, lang) : ''; + if (bookData) { + this.authorId = bookData.author_id; + this.type = bookData.type; + this.title = bookData.title ? System.decryptDataWithUserKey(bookData.title, userKey) : ''; + this.subTitle = bookData.sub_title ? System.decryptDataWithUserKey(bookData.sub_title, userKey) : ''; + this.summary = bookData.summary ? System.decryptDataWithUserKey(bookData.summary, userKey) : ''; + this.serieId = bookData.serie_id ?? 0; + this.desiredReleaseDate = bookData.desired_release_date ?? ''; + this.desiredWordCount = bookData.desired_word_count ?? 0; + this.wordCount = bookData.words_count ?? 0; + this.cover = bookData.cover_image ? Cover.getPicture(userId, userKey, bookData.cover_image, lang) : ''; } else { this.authorId = ''; this.title = ''; @@ -1107,1158 +448,4 @@ export default class Book { this.cover = ''; } } - - static async getSyncedBooks(userId: string, lang: 'fr' | 'en'):Promise { - const userKey: string = getUserEncryptionKey(userId); - - const [ - allBooks, - allChapters, - allChapterContents, - allChapterInfos, - allCharacters, - allCharacterAttributes, - allLocations, - allLocationElements, - allLocationSubElements, - allWorlds, - allWorldElements, - allIncidents, - allPlotPoints, - allIssues, - allActSummaries, - allGuidelines, - allAIGuidelines - ]: [ - SyncedBookResult[], - SyncedChapterResult[], - SyncedChapterContentResult[], - SyncedChapterInfoResult[], - SyncedCharacterResult[], - SyncedCharacterAttributeResult[], - SyncedLocationResult[], - SyncedLocationElementResult[], - SyncedLocationSubElementResult[], - SyncedWorldResult[], - SyncedWorldElementResult[], - SyncedIncidentResult[], - SyncedPlotPointResult[], - SyncedIssueResult[], - SyncedActSummaryResult[], - SyncedGuideLineResult[], - SyncedAIGuideLineResult[] - ] = await Promise.all([ - BookRepo.fetchSyncedBooks(userId,lang), - BookRepo.fetchSyncedChapters(userId,lang), - BookRepo.fetchSyncedChapterContents(userId,lang), - BookRepo.fetchSyncedChapterInfos(userId,lang), - BookRepo.fetchSyncedCharacters(userId,lang), - BookRepo.fetchSyncedCharacterAttributes(userId,lang), - BookRepo.fetchSyncedLocations(userId,lang), - BookRepo.fetchSyncedLocationElements(userId,lang), - BookRepo.fetchSyncedLocationSubElements(userId,lang), - BookRepo.fetchSyncedWorlds(userId,lang), - BookRepo.fetchSyncedWorldElements(userId,lang), - BookRepo.fetchSyncedIncidents(userId,lang), - BookRepo.fetchSyncedPlotPoints(userId,lang), - BookRepo.fetchSyncedIssues(userId,lang), - BookRepo.fetchSyncedActSummaries(userId,lang), - BookRepo.fetchSyncedGuideLine(userId,lang), - BookRepo.fetchSyncedAIGuideLine(userId,lang) - ]); - - return allBooks.map((book: SyncedBookResult): SyncedBook => { - const bookId: string = book.book_id; - - const chapters: SyncedChapter[] = allChapters - .filter((chapter: SyncedChapterResult): boolean => chapter.book_id === bookId) - .map((chapter: SyncedChapterResult): SyncedChapter => { - const chapterId: string = chapter.chapter_id; - - const contents: SyncedChapterContent[] = allChapterContents - .filter((content: SyncedChapterContentResult): boolean => content.chapter_id === chapterId) - .map((content: SyncedChapterContentResult): SyncedChapterContent => ({ - id: content.content_id, - lastUpdate: content.last_update - })); - - const infoData: SyncedChapterInfoResult | undefined = allChapterInfos.find((info: SyncedChapterInfoResult): boolean => info.chapter_id === chapterId); - const info: SyncedChapterInfo | null = infoData ? { - id: infoData.chapter_info_id, - lastUpdate: infoData.last_update - } : null; - - return { - id: chapterId, - name: System.decryptDataWithUserKey(chapter.title, userKey), - lastUpdate: chapter.last_update, - contents, - info - }; - }); - - const characters: SyncedCharacter[] = allCharacters - .filter((character: SyncedCharacterResult): boolean => character.book_id === bookId) - .map((character: SyncedCharacterResult): SyncedCharacter => { - const characterId: string = character.character_id; - - const attributes: SyncedCharacterAttribute[] = allCharacterAttributes - .filter((attribute: SyncedCharacterAttributeResult): boolean => attribute.character_id === characterId) - .map((attribute: SyncedCharacterAttributeResult): SyncedCharacterAttribute => ({ - id: attribute.attr_id, - name: System.decryptDataWithUserKey(attribute.attribute_name, userKey), - lastUpdate: attribute.last_update - })); - - return { - id: characterId, - name: System.decryptDataWithUserKey(character.first_name, userKey), - lastUpdate: character.last_update, - attributes - }; - }); - - const locations: SyncedLocation[] = allLocations - .filter((location: SyncedLocationResult): boolean => location.book_id === bookId) - .map((location: SyncedLocationResult): SyncedLocation => { - const locationId: string = location.loc_id; - - const elements: SyncedLocationElement[] = allLocationElements - .filter((element: SyncedLocationElementResult): boolean => element.location === locationId) - .map((element: SyncedLocationElementResult): SyncedLocationElement => { - const elementId: string = element.element_id; - - const subElements: SyncedLocationSubElement[] = allLocationSubElements - .filter((subElement: SyncedLocationSubElementResult): boolean => subElement.element_id === elementId) - .map((subElement: SyncedLocationSubElementResult): SyncedLocationSubElement => ({ - id: subElement.sub_element_id, - name: System.decryptDataWithUserKey(subElement.sub_elem_name, userKey), - lastUpdate: subElement.last_update - })); - - return { - id: elementId, - name: System.decryptDataWithUserKey(element.element_name, userKey), - lastUpdate: element.last_update, - subElements - }; - }); - - return { - id: locationId, - name: System.decryptDataWithUserKey(location.loc_name, userKey), - lastUpdate: location.last_update, - elements - }; - }); - - const worlds: SyncedWorld[] = allWorlds - .filter((world: SyncedWorldResult): boolean => world.book_id === bookId) - .map((world: SyncedWorldResult): SyncedWorld => { - const worldId: string = world.world_id; - - const elements: SyncedWorldElement[] = allWorldElements - .filter((worldElement: SyncedWorldElementResult): boolean => worldElement.world_id === worldId) - .map((worldElement: SyncedWorldElementResult): SyncedWorldElement => ({ - id: worldElement.element_id, - name: System.decryptDataWithUserKey(worldElement.name, userKey), - lastUpdate: worldElement.last_update - })); - - return { - id: worldId, - name: System.decryptDataWithUserKey(world.name, userKey), - lastUpdate: world.last_update, - elements - }; - }); - - const incidents: SyncedIncident[] = allIncidents - .filter((incident: SyncedIncidentResult): boolean => incident.book_id === bookId) - .map((incident: SyncedIncidentResult): SyncedIncident => ({ - id: incident.incident_id, - name: System.decryptDataWithUserKey(incident.title, userKey), - lastUpdate: incident.last_update - })); - - const plotPoints: SyncedPlotPoint[] = allPlotPoints - .filter((plotPoint: SyncedPlotPointResult): boolean => plotPoint.book_id === bookId) - .map((plotPoint: SyncedPlotPointResult): SyncedPlotPoint => ({ - id: plotPoint.plot_point_id, - name: System.decryptDataWithUserKey(plotPoint.title, userKey), - lastUpdate: plotPoint.last_update - })); - - const issues: SyncedIssue[] = allIssues - .filter((issue: SyncedIssueResult): boolean => issue.book_id === bookId) - .map((issue: SyncedIssueResult): SyncedIssue => ({ - id: issue.issue_id, - name: System.decryptDataWithUserKey(issue.name, userKey), - lastUpdate: issue.last_update - })); - - const actSummaries: SyncedActSummary[] = allActSummaries - .filter((actSummary: SyncedActSummaryResult): boolean => actSummary.book_id === bookId) - .map((actSummary: SyncedActSummaryResult): SyncedActSummary => ({ - id: actSummary.act_sum_id, - lastUpdate: actSummary.last_update - })); - - const guidelineData: SyncedGuideLineResult | undefined = allGuidelines.find((guideline: SyncedGuideLineResult): boolean => guideline.book_id === bookId); - const guideLine: SyncedGuideLine | null = guidelineData ? { - lastUpdate: guidelineData.last_update - } : null; - - const aiGuidelineData: SyncedAIGuideLineResult | undefined = allAIGuidelines.find((aiGuideline: SyncedAIGuideLineResult): boolean => aiGuideline.book_id === bookId); - const aiGuideLine: SyncedAIGuideLine | null = aiGuidelineData ? { - lastUpdate: aiGuidelineData.last_update - } : null; - - return { - id: bookId, - type: book.type, - title: System.decryptDataWithUserKey(book.title, userKey), - subTitle: book.sub_title ? System.decryptDataWithUserKey(book.sub_title, userKey) : null, - lastUpdate: book.last_update, - chapters, - characters, - locations, - worlds, - incidents, - plotPoints, - issues, - actSummaries, - guideLine, - aiGuideLine - }; - }); - } - - static async uploadBookForSync(userId:string,bookId: string,lang: "fr" | "en"): Promise { - const userKey: string = getUserEncryptionKey(userId); - const [ - eritBooksRaw, - actSummariesRaw, - aiGuideLineRaw, - chaptersRaw, - charactersRaw, - guideLineRaw, - incidentsRaw, - issuesRaw, - locationsRaw, - plotPointsRaw, - worldsRaw - ]: [ - EritBooksTable[], - BookActSummariesTable[], - BookAIGuideLineTable[], - BookChaptersTable[], - BookCharactersTable[], - BookGuideLineTable[], - BookIncidentsTable[], - BookIssuesTable[], - BookLocationTable[], - BookPlotPointsTable[], - BookWorldTable[] - ] = await Promise.all([ - BookRepo.fetchEritBooksTable(userId, bookId,lang), - BookRepo.fetchBookActSummaries(userId, bookId,lang), - BookRepo.fetchBookAIGuideLine(userId, bookId,lang), - BookRepo.fetchBookChapters(userId, bookId,lang), - BookRepo.fetchBookCharacters(userId, bookId,lang), - BookRepo.fetchBookGuideLineTable(userId, bookId,lang), - BookRepo.fetchBookIncidents(userId, bookId,lang), - BookRepo.fetchBookIssues(userId, bookId,lang), - BookRepo.fetchBookLocations(userId, bookId,lang), - BookRepo.fetchBookPlotPoints(userId, bookId,lang), - BookRepo.fetchBookWorlds(userId, bookId,lang) - ]); - - const [ - chapterContentsNested, - chapterInfosNested, - characterAttributesNested, - worldElementsNested, - locationElementsNested - ]: [ - BookChapterContentTable[][], - BookChapterInfosTable[][], - BookCharactersAttributesTable[][], - BookWorldElementsTable[][], - LocationElementTable[][] - ] = await Promise.all([ - Promise.all(chaptersRaw.map((chapter: BookChaptersTable): Promise => BookRepo.fetchBookChapterContents(userId, chapter.chapter_id,lang))), - Promise.all(chaptersRaw.map((chapter: BookChaptersTable): Promise => BookRepo.fetchBookChapterInfos(userId, chapter.chapter_id,lang))), - Promise.all(charactersRaw.map((character: BookCharactersTable): Promise => BookRepo.fetchBookCharactersAttributes(userId, character.character_id,lang))), - Promise.all(worldsRaw.map((world: BookWorldTable): Promise => BookRepo.fetchBookWorldElements(userId, world.world_id,lang))), - Promise.all(locationsRaw.map((location: BookLocationTable): Promise => BookRepo.fetchLocationElements(userId, location.loc_id,lang))) - ]); - - const chapterContentsRaw: BookChapterContentTable[] = chapterContentsNested.flat(); - const chapterInfosRaw: BookChapterInfosTable[] = chapterInfosNested.flat(); - const characterAttributesRaw: BookCharactersAttributesTable[] = characterAttributesNested.flat(); - const worldElementsRaw: BookWorldElementsTable[] = worldElementsNested.flat(); - const locationElementsRaw: LocationElementTable[] = locationElementsNested.flat(); - - const locationSubElementsNested: LocationSubElementTable[][] = await Promise.all( - locationElementsRaw.map((element: LocationElementTable): Promise => BookRepo.fetchLocationSubElements(userId, element.element_id,lang)) - ); - const locationSubElementsRaw: LocationSubElementTable[] = locationSubElementsNested.flat(); - - const eritBooks: EritBooksTable[] = eritBooksRaw.map((book: EritBooksTable): EritBooksTable => ({ - ...book, - title: System.decryptDataWithUserKey(book.title, userKey), - sub_title: book.sub_title ? System.decryptDataWithUserKey(book.sub_title, userKey) : null, - summary: book.summary ? System.decryptDataWithUserKey(book.summary, userKey) : null, - cover_image: book.cover_image ? System.decryptDataWithUserKey(book.cover_image, userKey) : null - })); - - const actSummaries: BookActSummariesTable[] = actSummariesRaw.map((actSummary: BookActSummariesTable): BookActSummariesTable => ({ - ...actSummary, - summary: actSummary.summary ? System.decryptDataWithUserKey(actSummary.summary, userKey) : null - })); - - const aiGuideLine: BookAIGuideLineTable[] = aiGuideLineRaw.map((guideLine: BookAIGuideLineTable): BookAIGuideLineTable => ({ - ...guideLine, - global_resume: guideLine.global_resume ? System.decryptDataWithUserKey(guideLine.global_resume, userKey) : null, - themes: guideLine.themes ? System.decryptDataWithUserKey(guideLine.themes, userKey) : null, - tone: guideLine.tone ? System.decryptDataWithUserKey(guideLine.tone, userKey) : null, - atmosphere: guideLine.atmosphere ? System.decryptDataWithUserKey(guideLine.atmosphere, userKey) : null, - current_resume: guideLine.current_resume ? System.decryptDataWithUserKey(guideLine.current_resume, userKey) : null - })); - - const chapters: BookChaptersTable[] = chaptersRaw.map((chapter: BookChaptersTable): BookChaptersTable => ({ - ...chapter, - title: System.decryptDataWithUserKey(chapter.title, userKey) - })); - - const chapterContents: BookChapterContentTable[] = chapterContentsRaw.map((chapterContent: BookChapterContentTable): BookChapterContentTable => ({ - ...chapterContent, - content: chapterContent.content ? JSON.parse(System.decryptDataWithUserKey(chapterContent.content, userKey)) : null - })); - - const chapterInfos: BookChapterInfosTable[] = chapterInfosRaw.map((chapterInfo: BookChapterInfosTable): BookChapterInfosTable => ({ - ...chapterInfo, - summary: chapterInfo.summary ? System.decryptDataWithUserKey(chapterInfo.summary, userKey) : null, - goal: chapterInfo.goal ? System.decryptDataWithUserKey(chapterInfo.goal, userKey) : null - })); - - const characters: BookCharactersTable[] = charactersRaw.map((character: BookCharactersTable): BookCharactersTable => ({ - ...character, - first_name: System.decryptDataWithUserKey(character.first_name, userKey), - last_name: character.last_name ? System.decryptDataWithUserKey(character.last_name, userKey) : null, - category: System.decryptDataWithUserKey(character.category, userKey), - title: character.title ? System.decryptDataWithUserKey(character.title, userKey) : null, - role: character.role ? System.decryptDataWithUserKey(character.role, userKey) : null, - biography: character.biography ? System.decryptDataWithUserKey(character.biography, userKey) : null, - history: character.history ? System.decryptDataWithUserKey(character.history, userKey) : null - })); - - const characterAttributes: BookCharactersAttributesTable[] = characterAttributesRaw.map((attribute: BookCharactersAttributesTable): BookCharactersAttributesTable => ({ - ...attribute, - attribute_name: System.decryptDataWithUserKey(attribute.attribute_name, userKey), - attribute_value: System.decryptDataWithUserKey(attribute.attribute_value, userKey) - })); - - const guideLine: BookGuideLineTable[] = guideLineRaw.map((guide: BookGuideLineTable): BookGuideLineTable => ({ - ...guide, - tone: guide.tone ? System.decryptDataWithUserKey(guide.tone, userKey) : null, - atmosphere: guide.atmosphere ? System.decryptDataWithUserKey(guide.atmosphere, userKey) : null, - writing_style: guide.writing_style ? System.decryptDataWithUserKey(guide.writing_style, userKey) : null, - themes: guide.themes ? System.decryptDataWithUserKey(guide.themes, userKey) : null, - symbolism: guide.symbolism ? System.decryptDataWithUserKey(guide.symbolism, userKey) : null, - motifs: guide.motifs ? System.decryptDataWithUserKey(guide.motifs, userKey) : null, - narrative_voice: guide.narrative_voice ? System.decryptDataWithUserKey(guide.narrative_voice, userKey) : null, - pacing: guide.pacing ? System.decryptDataWithUserKey(guide.pacing, userKey) : null, - intended_audience: guide.intended_audience ? System.decryptDataWithUserKey(guide.intended_audience, userKey) : null, - key_messages: guide.key_messages ? System.decryptDataWithUserKey(guide.key_messages, userKey) : null - })); - - const incidents: BookIncidentsTable[] = incidentsRaw.map((incident: BookIncidentsTable): BookIncidentsTable => ({ - ...incident, - title: System.decryptDataWithUserKey(incident.title, userKey), - summary: incident.summary ? System.decryptDataWithUserKey(incident.summary, userKey) : null - })); - - const issues: BookIssuesTable[] = issuesRaw.map((issue: BookIssuesTable): BookIssuesTable => ({ - ...issue, - name: System.decryptDataWithUserKey(issue.name, userKey) - })); - - const locations: BookLocationTable[] = locationsRaw.map((location: BookLocationTable): BookLocationTable => ({ - ...location, - loc_name: System.decryptDataWithUserKey(location.loc_name, userKey) - })); - - const plotPoints: BookPlotPointsTable[] = plotPointsRaw.map((plotPoint: BookPlotPointsTable): BookPlotPointsTable => ({ - ...plotPoint, - title: System.decryptDataWithUserKey(plotPoint.title, userKey), - summary: plotPoint.summary ? System.decryptDataWithUserKey(plotPoint.summary, userKey) : null - })); - - const worlds: BookWorldTable[] = worldsRaw.map((world: BookWorldTable): BookWorldTable => ({ - ...world, - name: System.decryptDataWithUserKey(world.name, userKey), - history: world.history ? System.decryptDataWithUserKey(world.history, userKey) : null, - politics: world.politics ? System.decryptDataWithUserKey(world.politics, userKey) : null, - economy: world.economy ? System.decryptDataWithUserKey(world.economy, userKey) : null, - religion: world.religion ? System.decryptDataWithUserKey(world.religion, userKey) : null, - languages: world.languages ? System.decryptDataWithUserKey(world.languages, userKey) : null - })); - - const worldElements: BookWorldElementsTable[] = worldElementsRaw.map((worldElement: BookWorldElementsTable): BookWorldElementsTable => ({ - ...worldElement, - name: System.decryptDataWithUserKey(worldElement.name, userKey), - description: worldElement.description ? System.decryptDataWithUserKey(worldElement.description, userKey) : null - })); - - const locationElements: LocationElementTable[] = locationElementsRaw.map((locationElement: LocationElementTable): LocationElementTable => ({ - ...locationElement, - element_name: System.decryptDataWithUserKey(locationElement.element_name, userKey), - element_description: locationElement.element_description ? System.decryptDataWithUserKey(locationElement.element_description, userKey) : null - })); - - const locationSubElements: LocationSubElementTable[] = locationSubElementsRaw.map((locationSubElement: LocationSubElementTable): LocationSubElementTable => ({ - ...locationSubElement, - sub_elem_name: System.decryptDataWithUserKey(locationSubElement.sub_elem_name, userKey), - sub_elem_description: locationSubElement.sub_elem_description ? System.decryptDataWithUserKey(locationSubElement.sub_elem_description, userKey) : null - })); - - return { - eritBooks, - actSummaries, - aiGuideLine, - chapters, - chapterContents, - chapterInfos, - characters, - characterAttributes, - guideLine, - incidents, - issues, - locations, - plotPoints, - worlds, - worldElements, - locationElements, - locationSubElements - }; - } - - static async saveCompleteBook(userId: string, data: CompleteBook, lang: "fr" | "en"):Promise { - const userKey: string = getUserEncryptionKey(userId); - - const book: EritBooksTable = data.eritBooks[0]; - const encryptedBookTitle: string = System.encryptDataWithUserKey(book.title, userKey); - const encryptedBookSubTitle: string | null = book.sub_title ? System.encryptDataWithUserKey(book.sub_title, userKey) : null; - const encryptedBookSummary: string | null = book.summary ? System.encryptDataWithUserKey(book.summary, userKey) : null; - const encryptedBookCoverImage: string | null = book.cover_image ? System.encryptDataWithUserKey(book.cover_image, userKey) : null; - - const bookInserted: boolean = BookRepo.insertSyncBook( - book.book_id, - userId, - book.type, - encryptedBookTitle, - book.hashed_title, - encryptedBookSubTitle, - book.hashed_sub_title, - encryptedBookSummary, - book.serie_id, - book.desired_release_date, - book.desired_word_count, - book.words_count, - encryptedBookCoverImage, - book.last_update, - lang - ); - if (!bookInserted) return false; - - const chaptersInserted: boolean = data.chapters.every((chapter: BookChaptersTable): boolean => { - const encryptedTitle: string = System.encryptDataWithUserKey(chapter.title, userKey); - return BookRepo.insertSyncChapter(chapter.chapter_id, chapter.book_id, userId, encryptedTitle, chapter.hashed_title, chapter.words_count, chapter.chapter_order, chapter.last_update, lang); - }); - if (!chaptersInserted) return false; - - const incidentsInserted: boolean = data.incidents.every((incident: BookIncidentsTable): boolean => { - const encryptedIncidentTitle: string = System.encryptDataWithUserKey(incident.title, userKey); - const encryptedIncidentSummary: string | null = incident.summary ? System.encryptDataWithUserKey(incident.summary, userKey) : null; - return BookRepo.insertSyncIncident(incident.incident_id, userId, incident.book_id, encryptedIncidentTitle, incident.hashed_title, encryptedIncidentSummary, incident.last_update, lang); - }); - if (!incidentsInserted) return false; - - const plotPointsInserted: boolean = data.plotPoints.every((plotPoint: BookPlotPointsTable): boolean => { - const encryptedPlotTitle: string = System.encryptDataWithUserKey(plotPoint.title, userKey); - const encryptedPlotSummary: string | null = plotPoint.summary ? System.encryptDataWithUserKey(plotPoint.summary, userKey) : null; - return BookRepo.insertSyncPlotPoint(plotPoint.plot_point_id, encryptedPlotTitle, plotPoint.hashed_title, encryptedPlotSummary, plotPoint.linked_incident_id, userId, plotPoint.book_id, plotPoint.last_update, lang); - }); - if (!plotPointsInserted) return false; - - const chapterContentsInserted: boolean = data.chapterContents.every((content: BookChapterContentTable): boolean => { - const encryptedContent: string | null = content.content ? System.encryptDataWithUserKey(JSON.stringify(content.content), userKey) : null; - return BookRepo.insertSyncChapterContent(content.content_id, content.chapter_id, userId, content.version, encryptedContent, content.words_count, content.time_on_it, content.last_update, lang); - }); - if (!chapterContentsInserted) return false; - - const chapterInfosInserted: boolean = data.chapterInfos.every((info: BookChapterInfosTable): boolean => { - const encryptedSummary: string | null = info.summary ? System.encryptDataWithUserKey(info.summary, userKey) : null; - const encryptedGoal: string | null = info.goal ? System.encryptDataWithUserKey(info.goal, userKey) : null; - return BookRepo.insertSyncChapterInfo(info.chapter_info_id, info.chapter_id, info.act_id, info.incident_id, info.plot_point_id, info.book_id, userId, encryptedSummary, encryptedGoal, info.last_update, lang); - }); - if (!chapterInfosInserted) return false; - - const charactersInserted: boolean = data.characters.every((character: BookCharactersTable): boolean => { - const encryptedFirstName: string = System.encryptDataWithUserKey(character.first_name, userKey); - const encryptedLastName: string | null = character.last_name ? System.encryptDataWithUserKey(character.last_name, userKey) : null; - const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey); - const encryptedCharTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, 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 encryptedCharHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userKey) : null; - return BookRepo.insertSyncCharacter(character.character_id, character.book_id, userId, encryptedFirstName, encryptedLastName, encryptedCategory, encryptedCharTitle, encryptedImage, encryptedRole, encryptedBiography, encryptedCharHistory, character.last_update, lang); - }); - if (!charactersInserted) return false; - - const characterAttributesInserted: boolean = data.characterAttributes.every((attr: BookCharactersAttributesTable): boolean => { - const encryptedAttrName: string = System.encryptDataWithUserKey(attr.attribute_name, userKey); - const encryptedAttrValue: string = System.encryptDataWithUserKey(attr.attribute_value, userKey); - return BookRepo.insertSyncCharacterAttribute(attr.attr_id, attr.character_id, userId, encryptedAttrName, encryptedAttrValue, attr.last_update, lang); - }); - if (!characterAttributesInserted) return false; - - const locationsInserted: boolean = data.locations.every((location: BookLocationTable): boolean => { - const encryptedLocName: string = System.encryptDataWithUserKey(location.loc_name, userKey); - return BookRepo.insertSyncLocation(location.loc_id, location.book_id, userId, encryptedLocName, location.loc_original_name, location.last_update, lang); - }); - if (!locationsInserted) return false; - - const locationElementsInserted: boolean = data.locationElements.every((element: LocationElementTable): boolean => { - const encryptedLocElemName: string = System.encryptDataWithUserKey(element.element_name, userKey); - const encryptedLocElemDesc: string | null = element.element_description ? System.encryptDataWithUserKey(element.element_description, userKey) : null; - return BookRepo.insertSyncLocationElement(element.element_id, element.location, userId, encryptedLocElemName, element.original_name, encryptedLocElemDesc, element.last_update, lang); - }); - if (!locationElementsInserted) return false; - - const locationSubElementsInserted: boolean = data.locationSubElements.every((subElement: LocationSubElementTable): boolean => { - const encryptedSubElemName: string = System.encryptDataWithUserKey(subElement.sub_elem_name, userKey); - const encryptedSubElemDesc: string | null = subElement.sub_elem_description ? System.encryptDataWithUserKey(subElement.sub_elem_description, userKey) : null; - return BookRepo.insertSyncLocationSubElement(subElement.sub_element_id, subElement.element_id, userId, encryptedSubElemName, subElement.original_name, encryptedSubElemDesc, subElement.last_update, lang); - }); - if (!locationSubElementsInserted) return false; - - const worldsInserted: boolean = data.worlds.every((world: BookWorldTable): boolean => { - const encryptedWorldName: string = System.encryptDataWithUserKey(world.name, userKey); - const encryptedWorldHistory: 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 BookRepo.insertSyncWorld(world.world_id, encryptedWorldName, world.hashed_name, userId, world.book_id, encryptedWorldHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, world.last_update, lang); - }); - if (!worldsInserted) return false; - - const worldElementsInserted: boolean = data.worldElements.every((element: BookWorldElementsTable): boolean => { - const encryptedElemName: string = System.encryptDataWithUserKey(element.name, userKey); - const encryptedElemDesc: string | null = element.description ? System.encryptDataWithUserKey(element.description, userKey) : null; - return BookRepo.insertSyncWorldElement(element.element_id, element.world_id, userId, element.element_type, encryptedElemName, element.original_name, encryptedElemDesc, element.last_update, lang); - }); - if (!worldElementsInserted) return false; - - const actSummariesInserted: boolean = data.actSummaries.every((actSummary: BookActSummariesTable): boolean => { - const encryptedSummary: string | null = actSummary.summary ? System.encryptDataWithUserKey(actSummary.summary, userKey) : null; - return BookRepo.insertSyncActSummary(actSummary.act_sum_id, actSummary.book_id, userId, actSummary.act_index, encryptedSummary, actSummary.last_update, lang); - }); - if (!actSummariesInserted) return false; - - const aiGuidelinesInserted: boolean = data.aiGuideLine.every((aiGuide: BookAIGuideLineTable): boolean => { - const encryptedGlobalResume: string | null = aiGuide.global_resume ? System.encryptDataWithUserKey(aiGuide.global_resume, userKey) : null; - const encryptedAIThemes: string | null = aiGuide.themes ? System.encryptDataWithUserKey(aiGuide.themes, userKey) : null; - const encryptedAITone: string | null = aiGuide.tone ? System.encryptDataWithUserKey(aiGuide.tone, userKey) : null; - const encryptedAIAtmosphere: string | null = aiGuide.atmosphere ? System.encryptDataWithUserKey(aiGuide.atmosphere, userKey) : null; - const encryptedCurrentResume: string | null = aiGuide.current_resume ? System.encryptDataWithUserKey(aiGuide.current_resume, userKey) : null; - return BookRepo.insertSyncAIGuideLine(userId, aiGuide.book_id, encryptedGlobalResume, encryptedAIThemes, aiGuide.verbe_tense, aiGuide.narrative_type, aiGuide.langue, aiGuide.dialogue_type, encryptedAITone, encryptedAIAtmosphere, encryptedCurrentResume, aiGuide.last_update, lang); - }); - if (!aiGuidelinesInserted) return false; - - const guidelinesInserted: boolean = data.guideLine.every((guide: BookGuideLineTable): boolean => { - const encryptedGuideTone: string | null = guide.tone ? System.encryptDataWithUserKey(guide.tone, userKey) : null; - const encryptedGuideAtmosphere: string | null = guide.atmosphere ? System.encryptDataWithUserKey(guide.atmosphere, userKey) : null; - const encryptedWritingStyle: string | null = guide.writing_style ? System.encryptDataWithUserKey(guide.writing_style, userKey) : null; - const encryptedGuideThemes: string | null = guide.themes ? System.encryptDataWithUserKey(guide.themes, userKey) : null; - const encryptedSymbolism: string | null = guide.symbolism ? System.encryptDataWithUserKey(guide.symbolism, userKey) : null; - const encryptedMotifs: string | null = guide.motifs ? System.encryptDataWithUserKey(guide.motifs, userKey) : null; - const encryptedNarrativeVoice: string | null = guide.narrative_voice ? System.encryptDataWithUserKey(guide.narrative_voice, userKey) : null; - const encryptedPacing: string | null = guide.pacing ? System.encryptDataWithUserKey(guide.pacing, userKey) : null; - const encryptedIntendedAudience: string | null = guide.intended_audience ? System.encryptDataWithUserKey(guide.intended_audience, userKey) : null; - const encryptedKeyMessages: string | null = guide.key_messages ? System.encryptDataWithUserKey(guide.key_messages, userKey) : null; - return BookRepo.insertSyncGuideLine(userId, guide.book_id, encryptedGuideTone, encryptedGuideAtmosphere, encryptedWritingStyle, encryptedGuideThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedIntendedAudience, encryptedKeyMessages, guide.last_update, lang); - }); - if (!guidelinesInserted) return false; - - return data.issues.every((issue: BookIssuesTable): boolean => { - const encryptedIssueName: string = System.encryptDataWithUserKey(issue.name, userKey); - return BookRepo.insertSyncIssue(issue.issue_id, userId, issue.book_id, encryptedIssueName, issue.hashed_issue_name, issue.last_update, lang); - }); - } - - static async getCompleteSyncBook(userId: string, data: BookSyncCompare, lang: "fr" | "en"):Promise { - const userKey: string = getUserEncryptionKey(userId); - const bookData: EritBooksTable[] = []; - const chaptersData: BookChaptersTable[] = []; - const plotPointsData: BookPlotPointsTable[] = []; - const incidentsData: BookIncidentsTable[] = []; - const chapterContentsData: BookChapterContentTable[] = []; - const chapterInfosData: BookChapterInfosTable[] = []; - const charactersData: BookCharactersTable[] = []; - const characterAttributesData: BookCharactersAttributesTable[] = []; - const locationsData: BookLocationTable[] = []; - const locationElementsData: LocationElementTable[] = []; - const locationSubElementsData: LocationSubElementTable[] = []; - const worldsData: BookWorldTable[] = []; - const worldElementsData: BookWorldElementsTable[] = []; - const actSummariesData: BookActSummariesTable[] = []; - const guideLineData: BookGuideLineTable[] = []; - const aiGuideLineData: BookAIGuideLineTable[] = []; - const issuesData: BookIssuesTable[] = []; - - const actSummaries: string[] = data.actSummaries; - const chapters: string[] = data.chapters; - const plotPoints: string[] = data.plotPoints; - const incidents: string[] = data.incidents; - const chapterContents: string[] = data.chapterContents; - const chapterInfos: string[] = data.chapterInfos; - const characters: string[] = data.characters; - const characterAttributes: string[] = data.characterAttributes; - const locations: string[] = data.locations; - const locationElements: string[] = data.locationElements; - const locationSubElements: string[] = data.locationSubElements; - const worlds: string[] = data.worlds; - const worldElements: string[] = data.worldElements; - const issues: string[] = data.issues; - - if (actSummaries && actSummaries.length > 0) { - for (const id of actSummaries) { - const actSummary: BookActSummariesTable[] = await BookRepo.fetchCompleteActSummaryById(id, lang); - if (actSummary.length>0) { - const actSummaryData: BookActSummariesTable = actSummary[0]; - actSummariesData.push({ - ...actSummaryData, - summary: actSummaryData.summary ? System.decryptDataWithUserKey(actSummaryData.summary, userKey) : null - }); - } - } - } - - if (chapters && chapters.length > 0) { - for (const id of chapters) { - const chapter: BookChaptersTable[] = await BookRepo.fetchCompleteChapterById(id, lang); - if (chapter.length>0) { - const chapterData: BookChaptersTable = chapter[0]; - chaptersData.push({ - ...chapterData, - title: System.decryptDataWithUserKey(chapterData.title, userKey) - }); - } - } - } - - if (plotPoints && plotPoints.length > 0) { - for (const id of plotPoints) { - const plotPoint: BookPlotPointsTable[] = await BookRepo.fetchCompletePlotPointById(id, lang); - if (plotPoint.length>0) { - const plotPointData: BookPlotPointsTable = plotPoint[0]; - plotPointsData.push({ - ...plotPointData, - title: System.decryptDataWithUserKey(plotPointData.title, userKey), - summary: plotPointData.summary ? System.decryptDataWithUserKey(plotPointData.summary, userKey) : null - }); - } - } - } - - if (incidents && incidents.length > 0) { - for (const id of incidents) { - const incident: BookIncidentsTable[] = await BookRepo.fetchCompleteIncidentById(id, lang); - if (incident.length>0) { - const incidentData: BookIncidentsTable = incident[0]; - incidentsData.push({ - ...incidentData, - title: System.decryptDataWithUserKey(incidentData.title, userKey), - summary: incidentData.summary ? System.decryptDataWithUserKey(incidentData.summary, userKey) : null - }); - } - } - } - - if (chapterContents && chapterContents.length > 0) { - for (const id of chapterContents) { - const chapterContent: BookChapterContentTable[] = await BookRepo.fetchCompleteChapterContentById(id, lang); - if (chapterContent.length>0) { - const chapterContentData: BookChapterContentTable = chapterContent[0]; - chapterContentsData.push({ - ...chapterContentData, - content: chapterContentData.content ? JSON.parse(System.decryptDataWithUserKey(chapterContentData.content, userKey)) : null - }); - } - } - } - - if (chapterInfos && chapterInfos.length > 0) { - for (const id of chapterInfos) { - const chapterInfo: BookChapterInfosTable[] = await BookRepo.fetchCompleteChapterInfoById(id, lang); - if (chapterInfo.length>0) { - const chapterInfoData: BookChapterInfosTable = chapterInfo[0]; - chapterInfosData.push({ - ...chapterInfoData, - summary: chapterInfoData.summary ? System.decryptDataWithUserKey(chapterInfoData.summary, userKey) : null, - goal: chapterInfoData.goal ? System.decryptDataWithUserKey(chapterInfoData.goal, userKey) : null - }); - } - } - } - - if (characters && characters.length > 0) { - for (const id of characters) { - const character: BookCharactersTable[] = await BookRepo.fetchCompleteCharacterById(id, lang); - if (character.length>0) { - const characterData: BookCharactersTable = character[0]; - charactersData.push({ - ...characterData, - first_name: System.decryptDataWithUserKey(characterData.first_name, userKey), - last_name: characterData.last_name ? System.decryptDataWithUserKey(characterData.last_name, userKey) : null, - category: System.decryptDataWithUserKey(characterData.category, userKey), - title: characterData.title ? System.decryptDataWithUserKey(characterData.title, userKey) : null, - role: characterData.role ? System.decryptDataWithUserKey(characterData.role, userKey) : null, - biography: characterData.biography ? System.decryptDataWithUserKey(characterData.biography, userKey) : null, - history: characterData.history ? System.decryptDataWithUserKey(characterData.history, userKey) : null - }); - } - } - } - - if (characterAttributes && characterAttributes.length > 0) { - for (const id of characterAttributes) { - const characterAttribute: BookCharactersAttributesTable[] = await BookRepo.fetchCompleteCharacterAttributeById(id, lang); - if (characterAttribute.length>0) { - const characterAttributeData: BookCharactersAttributesTable = characterAttribute[0]; - characterAttributesData.push({ - ...characterAttributeData, - attribute_name: System.decryptDataWithUserKey(characterAttributeData.attribute_name, userKey), - attribute_value: System.decryptDataWithUserKey(characterAttributeData.attribute_value, userKey) - }); - } - } - } - - if (locations && locations.length > 0) { - for (const id of locations) { - const location: BookLocationTable[] = await BookRepo.fetchCompleteLocationById(id, lang); - if (location.length>0) { - const locationData: BookLocationTable = location[0]; - locationsData.push({ - ...locationData, - loc_name: System.decryptDataWithUserKey(locationData.loc_name, userKey) - }); - } - } - } - - if (locationElements && locationElements.length > 0) { - for (const id of locationElements) { - const locationElement: LocationElementTable[] = await BookRepo.fetchCompleteLocationElementById(id, lang); - if (locationElement.length>0) { - const locationElementData: LocationElementTable = locationElement[0]; - locationElementsData.push({ - ...locationElementData, - element_name: System.decryptDataWithUserKey(locationElementData.element_name, userKey), - element_description: locationElementData.element_description ? System.decryptDataWithUserKey(locationElementData.element_description, userKey) : null - }); - } - } - } - - if (locationSubElements && locationSubElements.length > 0) { - for (const id of locationSubElements) { - const locationSubElement: LocationSubElementTable[] = await BookRepo.fetchCompleteLocationSubElementById(id, lang); - if (locationSubElement.length>0) { - const locationSubElementData: LocationSubElementTable = locationSubElement[0]; - locationSubElementsData.push({ - ...locationSubElementData, - sub_elem_name: System.decryptDataWithUserKey(locationSubElementData.sub_elem_name, userKey), - sub_elem_description: locationSubElementData.sub_elem_description ? System.decryptDataWithUserKey(locationSubElementData.sub_elem_description, userKey) : null - }); - } - } - } - - if (worlds && worlds.length > 0) { - for (const id of worlds) { - const world: BookWorldTable[] = await BookRepo.fetchCompleteWorldById(id, lang); - if (world.length>0) { - const worldData: BookWorldTable = world[0]; - worldsData.push({ - ...worldData, - name: System.decryptDataWithUserKey(worldData.name, userKey), - history: worldData.history ? System.decryptDataWithUserKey(worldData.history, userKey) : null, - politics: worldData.politics ? System.decryptDataWithUserKey(worldData.politics, userKey) : null, - economy: worldData.economy ? System.decryptDataWithUserKey(worldData.economy, userKey) : null, - religion: worldData.religion ? System.decryptDataWithUserKey(worldData.religion, userKey) : null, - languages: worldData.languages ? System.decryptDataWithUserKey(worldData.languages, userKey) : null - }); - } - } - } - - if (worldElements && worldElements.length > 0) { - for (const id of worldElements) { - const worldElement: BookWorldElementsTable[] = await BookRepo.fetchCompleteWorldElementById(id, lang); - if (worldElement.length>0) { - const worldElementData: BookWorldElementsTable = worldElement[0]; - worldElementsData.push({ - ...worldElementData, - name: System.decryptDataWithUserKey(worldElementData.name, userKey), - description: worldElementData.description ? System.decryptDataWithUserKey(worldElementData.description, userKey) : null - }); - } - } - } - - if (issues && issues.length > 0) { - for (const id of issues) { - const issue: BookIssuesTable[] = await BookRepo.fetchCompleteIssueById(id, lang); - if (issue.length>0) { - const issueData: BookIssuesTable = issue[0]; - issuesData.push({ - ...issueData, - name: System.decryptDataWithUserKey(issueData.name, userKey) - }); - } - } - } - const book: EritBooksTable[] = await BookRepo.fetchCompleteBookById(data.id, lang); - if (book.length>0) { - const bookDataItem: EritBooksTable = book[0]; - bookData.push({ - ...bookDataItem, - title: System.decryptDataWithUserKey(bookDataItem.title, userKey), - sub_title: bookDataItem.sub_title ? System.decryptDataWithUserKey(bookDataItem.sub_title, userKey) : null, - summary: bookDataItem.summary ? System.decryptDataWithUserKey(bookDataItem.summary, userKey) : null, - cover_image: bookDataItem.cover_image ? System.decryptDataWithUserKey(bookDataItem.cover_image, userKey) : null - }); - } - return { - eritBooks: bookData, - chapters: chaptersData, - plotPoints: plotPointsData, - incidents: incidentsData, - chapterContents: chapterContentsData, - chapterInfos: chapterInfosData, - characters: charactersData, - characterAttributes: characterAttributesData, - locations: locationsData, - locationElements: locationElementsData, - locationSubElements: locationSubElementsData, - worlds: worldsData, - worldElements: worldElementsData, - actSummaries: actSummariesData, - guideLine: guideLineData, - aiGuideLine: aiGuideLineData, - issues: issuesData - }; - } - - static async syncBookFromServerToClient(userId:string,completeBook: CompleteBook,lang:"fr"|"en"):Promise { - const userKey: string = getUserEncryptionKey(userId); - - const actSummaries: BookActSummariesTable[] = completeBook.actSummaries; - const chapters: BookChaptersTable[] = completeBook.chapters; - const plotPoints: BookPlotPointsTable[] = completeBook.plotPoints; - const incidents: BookIncidentsTable[] = completeBook.incidents; - const chapterContents: BookChapterContentTable[] = completeBook.chapterContents; - const chapterInfos: BookChapterInfosTable[] = completeBook.chapterInfos; - const characters: BookCharactersTable[] = completeBook.characters; - const characterAttributes: BookCharactersAttributesTable[] = completeBook.characterAttributes; - const locations: BookLocationTable[] = completeBook.locations; - const locationElements: LocationElementTable[] = completeBook.locationElements; - const locationSubElements: LocationSubElementTable[] = completeBook.locationSubElements; - const worlds: BookWorldTable[] = completeBook.worlds; - const worldElements: BookWorldElementsTable[] = completeBook.worldElements; - const issues: BookIssuesTable[] = completeBook.issues; - - const bookId: string = completeBook.eritBooks.length > 0 ? completeBook.eritBooks[0].book_id : ''; - if (chapters && chapters.length > 0) { - for (const chapter of chapters) { - const isExist: boolean = ChapterRepo.isChapterExist(userId, chapter.chapter_id,lang); - const title: string = System.encryptDataWithUserKey(chapter.title, userKey) - if (isExist) { - const updated: boolean = ChapterRepo.updateChapter(userId, chapter.chapter_id, title, chapter.hashed_title, chapter.chapter_order, chapter.last_update, lang); - if (!updated) { - return false; - } - } else { - const insert: boolean = BookRepo.insertSyncChapter(chapter.chapter_id, chapter.book_id, userId, title, chapter.hashed_title, chapter.words_count || 0, chapter.chapter_order, chapter.last_update,lang); - if (!insert) { - return false; - } - } - } - } - - if (actSummaries && actSummaries.length > 0) { - for (const act of actSummaries) { - const isExist: boolean = BookRepo.actSummarizeExist(userId, bookId, act.act_index,lang); - const summary: string = System.encryptDataWithUserKey(act.summary ? act.summary : '', userKey) - if (isExist) { - const updated: boolean = BookRepo.updateActSummary(userId, bookId, act.act_index, summary, act.last_update,lang); - if (!updated) { - return false; - } - } else { - const insert: boolean = BookRepo.insertSyncActSummary(act.act_sum_id, userId, bookId, act.act_index, summary, act.last_update,lang); - if (!insert) { - return false; - } - } - } - } - - if (plotPoints && plotPoints.length > 0) { - for (const plotPoint of plotPoints) { - const title: string = System.encryptDataWithUserKey(plotPoint.title, userKey); - const summary: string = System.encryptDataWithUserKey(plotPoint.summary ? plotPoint.summary : '', userKey); - const ifExist: boolean = BookRepo.plotPointExist(userId, bookId, plotPoint.plot_point_id,lang); - if (ifExist) { - const updated: boolean = BookRepo.updatePlotPoint(userId, bookId, plotPoint.plot_point_id, title, plotPoint.hashed_title, summary, plotPoint.last_update,lang); - if (!updated) { - return false; - } - } else { - if (!plotPoint.linked_incident_id) { - return false; - } - const created: boolean = BookRepo.insertSyncPlotPoint(plotPoint.plot_point_id, title, plotPoint.hashed_title, summary, plotPoint.linked_incident_id, plotPoint.author_id, bookId, plotPoint.last_update,lang); - if (!created) { - return false; - } - } - } - } - - if (incidents && incidents.length > 0) { - for (const incident of incidents) { - const title: string = System.encryptDataWithUserKey(incident.title, userKey); - const summary: string = System.encryptDataWithUserKey(incident.summary ? incident.summary : '', userKey); - const isExist: boolean = BookRepo.incidentExist(userId, bookId, incident.incident_id,lang); - if (isExist) { - const updated: boolean = BookRepo.updateIncident(userId, bookId, incident.incident_id, title, incident.hashed_title, summary, incident.last_update,lang); - if (!updated) { - return false; - } - } else { - const created: boolean = BookRepo.insertSyncIncident(incident.incident_id, userId, bookId, title, incident.hashed_title, summary, incident.last_update,lang); - if (!created) { - return false; - } - } - } - } - - if (chapterContents && chapterContents.length > 0) { - for (const chapterContent of chapterContents) { - const isExist: boolean = ChapterRepo.isChapterContentExist(userId, chapterContent.content_id, lang); - const content: string = System.encryptDataWithUserKey(chapterContent.content ? JSON.stringify(chapterContent.content) : '', userKey); - if (isExist) { - const updated: boolean = ChapterRepo.updateChapterContent(userId, chapterContent.chapter_id, chapterContent.version, content, chapterContent.words_count, chapterContent.last_update); - if (!updated) { - return false; - } - } else { - const insert: boolean = BookRepo.insertSyncChapterContent(chapterContent.content_id, chapterContent.chapter_id, userId, chapterContent.version, content, chapterContent.words_count, chapterContent.time_on_it, chapterContent.last_update, lang); - if (!insert) { - return false; - } - } - } - } - - if (chapterInfos && chapterInfos.length > 0) { - for (const chapterInfo of chapterInfos) { - const isExist: boolean = ChapterRepo.isChapterInfoExist(userId, chapterInfo.chapter_id,lang); - const summary: string = System.encryptDataWithUserKey(chapterInfo.summary ? chapterInfo.summary : '', userKey); - const goal: string = System.encryptDataWithUserKey(chapterInfo.goal ? chapterInfo.goal : '', userKey); - if (isExist) { - const updated: boolean = ChapterRepo.updateChapterInfos(userId, chapterInfo.chapter_id, chapterInfo.act_id, bookId, chapterInfo.incident_id, chapterInfo.plot_point_id, summary, goal, chapterInfo.last_update,lang); - if (!updated) { - return false; - } - } else { - const insert: boolean = BookRepo.insertSyncChapterInfo(chapterInfo.chapter_info_id, chapterInfo.chapter_id, chapterInfo.act_id, chapterInfo.incident_id, chapterInfo.plot_point_id, bookId, chapterInfo.author_id, summary, goal, chapterInfo.last_update,lang); - if (!insert) { - return false; - } - } - } - } - - if (characters && characters.length > 0) { - for (const character of characters) { - const isExist: boolean = CharacterRepo.isCharacterExist(userId, character.character_id,lang); - const firstName: string = System.encryptDataWithUserKey(character.first_name, userKey); - const lastName: string = System.encryptDataWithUserKey(character.last_name ? character.last_name : '', userKey); - const category: string = System.encryptDataWithUserKey(character.category, userKey); - const title: string = System.encryptDataWithUserKey(character.title ? character.title : '', userKey); - const role: string = System.encryptDataWithUserKey(character.role ? character.role : '', userKey); - const image: string = System.encryptDataWithUserKey(character.image ? character.image : '', userKey); - const biography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userKey); - const history: string = System.encryptDataWithUserKey(character.history ? character.history : '', userKey); - if (isExist) { - const updated: boolean = CharacterRepo.updateCharacter(userId, character.character_id, firstName, lastName, title, category, image, role, biography, history, character.last_update); - if (!updated) { - return false; - } - } else { - const insert: boolean = BookRepo.insertSyncCharacter(character.character_id, bookId, userId, firstName, lastName, category, title, image, role, biography, history, character.last_update,lang); - if (!insert) { - return false; - } - } - } - } - - if (characterAttributes && characterAttributes.length > 0) { - for (const characterAttribute of characterAttributes) { - const isExist: boolean = CharacterRepo.isCharacterAttributeExist(userId, characterAttribute.attr_id,lang); - const attributeName: string = System.encryptDataWithUserKey(characterAttribute.attribute_name, userKey); - const attributeValue: string = System.encryptDataWithUserKey(characterAttribute.attribute_value, userKey); - if (isExist) { - const updated: boolean = CharacterRepo.updateCharacterAttribute(userId, characterAttribute.attr_id, attributeName, attributeValue, characterAttribute.last_update,lang); - if (!updated) { - return false; - } - } else { - const insert: boolean = BookRepo.insertSyncCharacterAttribute(characterAttribute.attr_id, characterAttribute.character_id, userId, attributeName, attributeValue, characterAttribute.last_update,lang); - if (!insert) { - return false; - } - } - } - } - - if (locations && locations.length > 0) { - for (const location of locations) { - const isExist: boolean = LocationRepo.isLocationExist(userId, location.loc_id,lang); - const locName: string = System.encryptDataWithUserKey(location.loc_name, userKey); - if (isExist) { - const updated: boolean = LocationRepo.updateLocationSection(userId, location.loc_id, locName, location.loc_original_name, location.last_update,lang); - if (!updated) { - return false; - } - } else { - const insert: boolean = BookRepo.insertSyncLocation(location.loc_id, bookId, userId, locName, location.loc_original_name, location.last_update,lang); - if (!insert) { - return false; - } - } - } - } - - if (locationElements && locationElements.length > 0) { - for (const locationElement of locationElements) { - const isExist: boolean = LocationRepo.isLocationElementExist(userId, locationElement.element_id,lang); - const elementName: string = System.encryptDataWithUserKey(locationElement.element_name, userKey); - const elementDescription: string = System.encryptDataWithUserKey(locationElement.element_description ? locationElement.element_description : '', userKey); - if (isExist) { - const updated: boolean = LocationRepo.updateLocationElement(userId, locationElement.element_id, elementName, locationElement.original_name, elementDescription, locationElement.last_update,lang); - if (!updated) { - return false; - } - } else { - const insert: boolean = BookRepo.insertSyncLocationElement(locationElement.element_id, locationElement.location, userId, elementName, locationElement.original_name, elementDescription, locationElement.last_update,lang); - if (!insert) { - return false; - } - } - } - } - - if (locationSubElements && locationSubElements.length > 0) { - for (const locationSubElement of locationSubElements) { - const isExist: boolean = LocationRepo.isLocationSubElementExist(userId, locationSubElement.sub_element_id,lang); - const subElemName: string = System.encryptDataWithUserKey(locationSubElement.sub_elem_name, userKey); - const subElemDescription: string = System.encryptDataWithUserKey(locationSubElement.sub_elem_description ? locationSubElement.sub_elem_description : '', userKey); - if (isExist) { - const updated: boolean = LocationRepo.updateLocationSubElement(userId, locationSubElement.sub_element_id, subElemName, locationSubElement.original_name, subElemDescription, locationSubElement.last_update,lang); - if (!updated) { - return false; - } - } else { - const insert: boolean = BookRepo.insertSyncLocationSubElement(locationSubElement.sub_element_id, locationSubElement.element_id, userId, subElemName, locationSubElement.original_name, subElemDescription, locationSubElement.last_update,lang); - if (!insert) { - return false; - } - } - } - } - - if (worlds && worlds.length > 0) { - for (const world of worlds) { - const isExist: boolean = BookRepo.worldExist(userId, bookId, world.world_id,lang); - const name: string = System.encryptDataWithUserKey(world.name, userKey); - const history: string = System.encryptDataWithUserKey(world.history ? world.history : '', userKey); - const politics: string = System.encryptDataWithUserKey(world.politics ? world.politics : '', userKey); - const economy: string = System.encryptDataWithUserKey(world.economy ? world.economy : '', userKey); - const religion: string = System.encryptDataWithUserKey(world.religion ? world.religion : '', userKey); - const languages: string = System.encryptDataWithUserKey(world.languages ? world.languages : '', userKey); - if (isExist) { - const updated: boolean = BookRepo.updateWorld(userId, world.world_id, name, world.hashed_name, history, politics, economy, religion, languages, world.last_update,lang); - if (!updated) { - return false; - } - } else { - const insert: boolean = BookRepo.insertSyncWorld(world.world_id, name, world.hashed_name, userId, bookId, history, politics, economy, religion, languages, world.last_update,lang); - if (!insert) { - return false; - } - } - } - } - - if (worldElements && worldElements.length > 0) { - for (const worldElement of worldElements) { - const isExist: boolean = BookRepo.worldElementExist(userId, worldElement.world_id, worldElement.element_id,lang); - const name: string = System.encryptDataWithUserKey(worldElement.name, userKey); - const description: string = System.encryptDataWithUserKey(worldElement.description ? worldElement.description : '', userKey); - if (isExist) { - const updated: boolean = BookRepo.updateWorldElement(userId, worldElement.element_id, name, description, worldElement.last_update,lang); - if (!updated) { - return false; - } - } else { - const insert: boolean = BookRepo.insertSyncWorldElement(worldElement.element_id, worldElement.world_id, userId, worldElement.element_type, name, worldElement.original_name, description, worldElement.last_update,lang); - if (!insert) { - return false; - } - } - } - } - - if (issues && issues.length > 0) { - for (const issue of issues) { - const isExist: boolean = BookRepo.issueExist(userId, bookId, issue.issue_id,lang); - const name: string = System.encryptDataWithUserKey(issue.name, userKey); - if (isExist) { - const updated: boolean = BookRepo.updateIssue(userId, bookId, issue.issue_id, name, issue.hashed_issue_name, issue.last_update, lang); - if (!updated) { - return false; - } - } else { - const insert: boolean = BookRepo.insertSyncIssue(issue.issue_id, userId, bookId, name, issue.hashed_issue_name, issue.last_update, lang); - if (!insert) { - return false; - } - } - } - } - return true; - } -} \ No newline at end of file +} diff --git a/electron/database/models/Chapter.ts b/electron/database/models/Chapter.ts index 7c64bd3..cf794cb 100644 --- a/electron/database/models/Chapter.ts +++ b/electron/database/models/Chapter.ts @@ -1,14 +1,18 @@ +import System from "../System.js"; +import { getUserEncryptionKey } from "../keyManager.js"; +import Book, { CompleteBookData } from "./Book.js"; import ChapterRepo, { ActChapterQuery, ChapterQueryResult, - ChapterContentQueryResult, - LastChapterResult, - CompanionContentQueryResult, ChapterStoryQueryResult, - ContentQueryResult + LastChapterResult } from "../repositories/chapter.repository.js"; -import System from "../System.js"; -import {getUserEncryptionKey} from "../keyManager.js"; +import { ActChapter, ActStory } from "./Act.js"; +import ChapterContentRepository, { + ChapterContentQueryResult, + CompanionContentQueryResult, + ContentQueryResult +} from "../repositories/chaptercontent.repository.js"; export interface ChapterContent { version: number; @@ -28,287 +32,427 @@ export interface ChapterProps { chapterContent?: ChapterContent } -export interface ActChapter { - chapterInfoId: number; - chapterId: string; - title: string; - chapterOrder: number; - actId: number; - incidentId: string | null; - plotPointId: string | null; - summary: string; - goal: string; -} - export interface CompanionContent { version: number; content: string; wordsCount: number; } -export interface ActStory { - actId: number; - summary: string; - chapterSummary: string; - chapterGoal: string; - incidents: IncidentStory[]; - plotPoints: PlotPointStory[]; +export interface SyncedChapter { + id: string; + name: string; + lastUpdate: number; + contents: SyncedChapterContent[]; + info: SyncedChapterInfo | null; } -export interface IncidentStory { - incidentTitle: string; - incidentSummary: string; - chapterSummary: string; - chapterGoal: string; +export interface SyncedChapterContent { + id: string; + lastUpdate: number; } -export interface PlotPointStory { - plotTitle: string; - plotSummary: string; - chapterSummary: string; - chapterGoal: string; +export interface SyncedChapterInfo { + id: string; + lastUpdate: number; +} + +export interface CompleteChapterContent { + id: string; + title: string; + content: string; + order: number; + version?: number; +} + +interface TipTapNode { + type?: string; + text?: string; + content?: TipTapNode[]; + attrs?: Record; + marks?: TipTapMark[]; +} + +interface TipTapMark { + type: string; + attrs?: Record; } export default class Chapter { + /** + * Retrieves all chapters from a specific book. + * @param userId - The unique identifier of the user + * @param bookId - The unique identifier of the book + * @param lang - The language for error messages ('fr' or 'en') + * @returns An array of ChapterProps containing chapter details + */ public static getAllChaptersFromABook(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps[] { - const chapters: ChapterQueryResult[] = ChapterRepo.fetchAllChapterFromABook(userId, bookId, lang); - let returnChapters: ChapterProps[] = []; - const userKey: string = getUserEncryptionKey(userId); - for (const chapter of chapters) { - const title: string = System.decryptDataWithUserKey(chapter.title, userKey); - returnChapters.push({ - chapterId: chapter.chapter_id, - title: title, - chapterOrder: chapter.chapter_order + const chapterQueryResults: ChapterQueryResult[] = ChapterRepo.fetchAllChapterFromABook(userId, bookId, lang); + const decryptedChapters: ChapterProps[] = []; + const userEncryptionKey: string = getUserEncryptionKey(userId); + + for (const chapterResult of chapterQueryResults) { + const decryptedTitle: string = System.decryptDataWithUserKey(chapterResult.title, userEncryptionKey); + decryptedChapters.push({ + chapterId: chapterResult.chapter_id, + title: decryptedTitle, + chapterOrder: chapterResult.chapter_order }); } - return returnChapters; + + return decryptedChapters; } + /** + * Retrieves all chapters organized by acts for a specific book. + * Caches decrypted titles to avoid redundant decryption operations. + * @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 ActChapter containing chapter details with act information + */ public static getAllChapterFromActs(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ActChapter[] { - const query: ActChapterQuery[] = ChapterRepo.fetchAllChapterForActs(userId, bookId, lang); - let chapters: ActChapter[] = []; - let tempChapter: { id: string, title: string }[] = [] - const userKey: string = getUserEncryptionKey(userId); - if (query.length > 0) { - for (const chapter of query) { - let decryptTitle: string = ''; - const newTitleId: number = tempChapter.findIndex((temp: { id: string, title: string }) => temp.id === chapter.chapter_id); - if (newTitleId > -1) { - decryptTitle = tempChapter[newTitleId]?.title ?? '' - } else { - decryptTitle = System.decryptDataWithUserKey(chapter.title, userKey); - tempChapter.push({id: chapter.chapter_id, title: decryptTitle}); - } - chapters.push({ - chapterId: chapter.chapter_id, - title: decryptTitle, - actId: chapter.act_id, - chapterInfoId: chapter.chapter_info_id, - chapterOrder: chapter.chapter_order, - goal: chapter.goal ? System.decryptDataWithUserKey(chapter.goal, userKey) : '', - summary: chapter.summary ? System.decryptDataWithUserKey(chapter.summary, userKey) : '', - incidentId: chapter.incident_id, - plotPointId: chapter.plot_point_id - }) - } - return chapters; - } else { - return [] + const actChapterQueryResults: ActChapterQuery[] = ChapterRepo.fetchAllChapterForActs(userId, bookId, lang); + const actChapters: ActChapter[] = []; + const decryptedTitleCache: { id: string; title: string }[] = []; + const userEncryptionKey: string = getUserEncryptionKey(userId); + + if (actChapterQueryResults.length === 0) { + return []; } + + for (const chapterQueryResult of actChapterQueryResults) { + let decryptedTitle: string = ''; + const cachedTitleIndex: number = decryptedTitleCache.findIndex( + (cachedItem: { id: string; title: string }) => cachedItem.id === chapterQueryResult.chapter_id + ); + + if (cachedTitleIndex > -1) { + decryptedTitle = decryptedTitleCache[cachedTitleIndex]?.title ?? ''; + } else { + decryptedTitle = System.decryptDataWithUserKey(chapterQueryResult.title, userEncryptionKey); + decryptedTitleCache.push({ id: chapterQueryResult.chapter_id, title: decryptedTitle }); + } + + actChapters.push({ + chapterId: chapterQueryResult.chapter_id, + title: decryptedTitle, + actId: chapterQueryResult.act_id, + chapterInfoId: chapterQueryResult.chapter_info_id, + chapterOrder: chapterQueryResult.chapter_order, + goal: chapterQueryResult.goal ? System.decryptDataWithUserKey(chapterQueryResult.goal, userEncryptionKey) : '', + summary: chapterQueryResult.summary ? System.decryptDataWithUserKey(chapterQueryResult.summary, userEncryptionKey) : '', + incidentId: chapterQueryResult.incident_id, + plotPointId: chapterQueryResult.plot_point_id + }); + } + + return actChapters; } + /** + * Retrieves a complete chapter with its content for a specific version. + * Optionally updates the last chapter record for the book. + * @param userId - The unique identifier of the user + * @param chapterId - The unique identifier of the chapter + * @param version - The version number of the chapter content + * @param bookId - Optional book identifier to update last chapter record + * @param lang - The language for error messages ('fr' or 'en') + * @returns ChapterProps containing chapter details and content + */ public static getWholeChapter(userId: string, chapterId: string, version: number, bookId?: string, lang: 'fr' | 'en' = 'fr'): ChapterProps { - const chapter: ChapterContentQueryResult = ChapterRepo.fetchWholeChapter(userId, chapterId, version, lang); - const userKey: string = getUserEncryptionKey(userId); + const chapterContentResult: ChapterContentQueryResult = ChapterContentRepository.fetchWholeChapter(userId, chapterId, version, lang); + const userEncryptionKey: string = getUserEncryptionKey(userId); if (bookId) { ChapterRepo.updateLastChapterRecord(userId, bookId, chapterId, version, lang); } + return { - chapterId: chapter.chapter_id, - title: System.decryptDataWithUserKey(chapter.title, userKey), - chapterOrder: chapter.chapter_order, + chapterId: chapterContentResult.chapter_id, + title: System.decryptDataWithUserKey(chapterContentResult.title, userEncryptionKey), + chapterOrder: chapterContentResult.chapter_order, chapterContent: { - content: chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '', + content: chapterContentResult.content ? System.decryptDataWithUserKey(chapterContentResult.content, userEncryptionKey) : '', version: version, - wordsCount: chapter.words_count + wordsCount: chapterContentResult.words_count } }; } + /** + * Saves the content of a chapter for a specific version. + * Encrypts the content before storing it in the database. + * @param userId - The unique identifier of the user + * @param chapterId - The unique identifier of the chapter + * @param version - The version number of the chapter content + * @param content - The JSON content to save + * @param wordsCount - The word count of the content + * @param currentTime - The current timestamp (unused, actual timestamp is generated) + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the content was saved successfully, false otherwise + */ public static saveChapterContent(userId: string, chapterId: string, version: number, content: JSON, wordsCount: number, currentTime: number, lang: 'fr' | 'en' = 'fr'): boolean { - const userKey: string = getUserEncryptionKey(userId); - const encryptContent: string = System.encryptDataWithUserKey(JSON.stringify(content), userKey); - /*if (version === 2){ - const QS = new AI(); - const prompt:string = System.htmlToText(Chapter.tipTapToHtml(content)); - const response:string = await QS.request(prompt,'summary-chapter'); - console.log(response); - }*/ - return ChapterRepo.updateChapterContent(userId, chapterId, version, encryptContent, wordsCount, System.timeStampInSeconds(), lang); + const userEncryptionKey: string = getUserEncryptionKey(userId); + const encryptedContent: string = System.encryptDataWithUserKey(JSON.stringify(content), userEncryptionKey); + return ChapterContentRepository.updateChapterContent(userId, chapterId, version, encryptedContent, wordsCount, System.timeStampInSeconds(), lang); } + /** + * Retrieves the last accessed chapter for a specific book. + * Falls back to the first chapter content if no last chapter record exists. + * @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 ChapterProps containing chapter details and content, or null if no chapters exist + */ public static getLastChapter(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps | null { - const lastChapter: LastChapterResult | null = ChapterRepo.fetchLastChapter(userId, bookId, lang); - if (lastChapter) { - return Chapter.getWholeChapter(userId, lastChapter.chapter_id, lastChapter.version, bookId, lang); + const lastChapterRecord: LastChapterResult | null = ChapterRepo.fetchLastChapter(userId, bookId, lang); + + if (lastChapterRecord) { + return Chapter.getWholeChapter(userId, lastChapterRecord.chapter_id, lastChapterRecord.version, bookId, lang); } - const chapter: ChapterContentQueryResult[] = ChapterRepo.fetchLastChapterContent(userId, bookId, lang); - if (chapter.length === 0) { - return null + + const chapterContentResults: ChapterContentQueryResult[] = ChapterContentRepository.fetchLastChapterContent(userId, bookId, lang); + + if (chapterContentResults.length === 0) { + return null; } - const chapterData: ChapterContentQueryResult = chapter[0]; - const userKey: string = getUserEncryptionKey(userId); + + const firstChapterContent: ChapterContentQueryResult = chapterContentResults[0]; + const userEncryptionKey: string = getUserEncryptionKey(userId); + return { - chapterId: chapterData.chapter_id, - title: chapterData.title ? System.decryptDataWithUserKey(chapterData.title, userKey) : '', - chapterOrder: chapterData.chapter_order, + chapterId: firstChapterContent.chapter_id, + title: firstChapterContent.title ? System.decryptDataWithUserKey(firstChapterContent.title, userEncryptionKey) : '', + chapterOrder: firstChapterContent.chapter_order, chapterContent: { - content: chapterData.content ? System.decryptDataWithUserKey(chapterData.content, userKey) : '', - version: chapterData.version, - wordsCount: chapterData.words_count + content: firstChapterContent.content ? System.decryptDataWithUserKey(firstChapterContent.content, userEncryptionKey) : '', + version: firstChapterContent.version, + wordsCount: firstChapterContent.words_count } }; } + /** + * Adds a new chapter to a book. + * Validates that the chapter name is unique within the book. + * @param userId - The unique identifier of the user + * @param bookId - The unique identifier of the book + * @param title - The title of the new chapter + * @param wordsCount - The initial word count of the chapter + * @param chapterOrder - The order position of the chapter + * @param lang - The language for error messages ('fr' or 'en') + * @param existingChapterId - Optional existing chapter ID for updates + * @returns The unique identifier of the created chapter + * @throws Error if a chapter with the same name already exists + */ public static addChapter(userId: string, bookId: string, title: string, wordsCount: number, chapterOrder: number, lang: 'fr' | 'en' = 'fr', existingChapterId?: string): string { const hashedTitle: string = System.hashElement(title); - const userKey: string = getUserEncryptionKey(userId); - const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey); + const userEncryptionKey: string = getUserEncryptionKey(userId); + const encryptedTitle: string = System.encryptDataWithUserKey(title, userEncryptionKey); if (!existingChapterId && ChapterRepo.checkNameDuplication(userId, bookId, hashedTitle, lang)) { throw new Error(lang === 'fr' ? `Ce nom de chapitre existe déjà.` : `This chapter name already exists.`); } + const chapterId: string = existingChapterId || System.createUniqueId(); return ChapterRepo.insertChapter(chapterId, userId, bookId, encryptedTitle, hashedTitle, wordsCount, chapterOrder, lang); } + /** + * Removes a chapter from the database. + * @param userId - The unique identifier of the user + * @param chapterId - The unique identifier of the chapter to remove + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the chapter was removed successfully, false otherwise + */ public static removeChapter(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): boolean { return ChapterRepo.deleteChapter(userId, chapterId, lang); } + /** + * Adds chapter information linking a chapter to an act, plot point, and/or incident. + * @param userId - The unique identifier of the user + * @param chapterId - The unique identifier of the chapter + * @param actId - The act number the chapter belongs to + * @param bookId - The unique identifier of the book + * @param plotId - Optional plot point identifier + * @param incidentId - Optional incident identifier + * @param lang - The language for error messages ('fr' or 'en') + * @param existingChapterInfoId - Optional existing chapter info ID for updates + * @returns The unique identifier of the created chapter information + */ public static addChapterInformation(userId: string, chapterId: string, actId: number, bookId: string, plotId: string | null, incidentId: string | null, lang: 'fr' | 'en' = 'fr', existingChapterInfoId?: string): string { const chapterInfoId: string = existingChapterInfoId || System.createUniqueId(); return ChapterRepo.insertChapterInformation(chapterInfoId, userId, chapterId, actId, bookId, plotId, incidentId, lang); } + /** + * Updates a chapter's title and order position. + * @param userId - The unique identifier of the user + * @param chapterId - The unique identifier of the chapter + * @param title - The new title for the chapter + * @param chapterOrder - The new order position for the chapter + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the chapter was updated successfully, false otherwise + */ public static updateChapter(userId: string, chapterId: string, title: string, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): boolean { const hashedTitle: string = System.hashElement(title); - const userKey: string = getUserEncryptionKey(userId); - const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey); + const userEncryptionKey: string = getUserEncryptionKey(userId); + const encryptedTitle: string = System.encryptDataWithUserKey(title, userEncryptionKey); return ChapterRepo.updateChapter(userId, chapterId, encryptedTitle, hashedTitle, chapterOrder, System.timeStampInSeconds(), lang); } - static updateChapterInfos(chapters: ActChapter[], userId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, lang: 'fr' | 'en' = 'fr') { - const userKey: string = getUserEncryptionKey(userId); - for (const chapter of chapters) { - const summary: string = chapter.summary ? System.encryptDataWithUserKey(chapter.summary, userKey) : ''; - const goal: string = chapter.goal ? System.encryptDataWithUserKey(chapter.goal, userKey) : ''; - const chapterId: string = chapter.chapterId; - ChapterRepo.updateChapterInfos(userId, chapterId, actId, bookId, incidentId, plotId, summary, goal, System.timeStampInSeconds(), lang); + /** + * Updates chapter information for multiple chapters including summary and goal. + * @param chapters - Array of ActChapter objects containing updated information + * @param userId - The unique identifier of the user + * @param actId - The act number the chapters belong to + * @param bookId - The unique identifier of the book + * @param incidentId - Optional incident identifier + * @param plotId - Optional plot point identifier + * @param lang - The language for error messages ('fr' or 'en') + */ + static updateChapterInfos(chapters: ActChapter[], userId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, lang: 'fr' | 'en' = 'fr'): void { + const userEncryptionKey: string = getUserEncryptionKey(userId); + + for (const chapterData of chapters) { + const encryptedSummary: string = chapterData.summary ? System.encryptDataWithUserKey(chapterData.summary, userEncryptionKey) : ''; + const encryptedGoal: string = chapterData.goal ? System.encryptDataWithUserKey(chapterData.goal, userEncryptionKey) : ''; + const chapterId: string = chapterData.chapterId; + ChapterRepo.updateChapterInfos(userId, chapterId, actId, bookId, incidentId, plotId, encryptedSummary, encryptedGoal, System.timeStampInSeconds(), lang); } } + /** + * Retrieves the companion content for a chapter (previous version content). + * @param userId - The unique identifier of the user + * @param chapterId - The unique identifier of the chapter + * @param version - The current version number (companion is version - 1) + * @param lang - The language for error messages ('fr' or 'en') + * @returns CompanionContent containing the previous version's content + */ static getCompanionContent(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): CompanionContent { - const versionNum: number = version - 1; - const chapterResponse: CompanionContentQueryResult[] = ChapterRepo.fetchCompanionContent(userId, chapterId, versionNum, lang); - if (chapterResponse.length === 0) { + const companionVersion: number = version - 1; + const companionContentResults: CompanionContentQueryResult[] = ChapterContentRepository.fetchCompanionContent(userId, chapterId, companionVersion, lang); + + if (companionContentResults.length === 0) { return { version: version, content: '', wordsCount: 0 }; } - const chapter: CompanionContentQueryResult = chapterResponse[0]; - const userKey: string = getUserEncryptionKey(userId); + + const companionContentData: CompanionContentQueryResult = companionContentResults[0]; + const userEncryptionKey: string = getUserEncryptionKey(userId); + return { - version: chapter.version, - content: chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '', - wordsCount: chapter.words_count + version: companionContentData.version, + content: companionContentData.content ? System.decryptDataWithUserKey(companionContentData.content, userEncryptionKey) : '', + wordsCount: companionContentData.words_count }; } + /** + * Retrieves the story context for a chapter including act summaries, incidents, and plot points. + * @param userId - The unique identifier of the user + * @param chapterId - The unique identifier of the chapter + * @param lang - The language for error messages ('fr' or 'en') + * @returns An array of ActStory containing story context organized by act + */ static getChapterStory(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): ActStory[] { - const stories: ChapterStoryQueryResult[] = ChapterRepo.fetchChapterStory(userId, chapterId, lang); - const actStories: Record = {}; - const userKey: string = getUserEncryptionKey(userId); + const chapterStoryResults: ChapterStoryQueryResult[] = ChapterRepo.fetchChapterStory(userId, chapterId, lang); + const actStoriesMap: Record = {}; + const userEncryptionKey: string = getUserEncryptionKey(userId); - for (const story of stories) { - const actId: number = story.act_id; + for (const storyResult of chapterStoryResults) { + const actId: number = storyResult.act_id; - if (!actStories[actId]) { - actStories[actId] = { + if (!actStoriesMap[actId]) { + actStoriesMap[actId] = { actId: actId, - summary: story.summary ? System.decryptDataWithUserKey(story.summary, userKey) : '', - chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '', - chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : '', + summary: storyResult.summary ? System.decryptDataWithUserKey(storyResult.summary, userEncryptionKey) : '', + chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '', + chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : '', incidents: [], plotPoints: [] }; } - if (story.incident_id) { - const incidentTitle = story.incident_title ? System.decryptDataWithUserKey(story.incident_title, userKey) : ''; - const incidentSummary = story.incident_summary ? System.decryptDataWithUserKey(story.incident_summary, userKey) : ''; + if (storyResult.incident_id) { + const decryptedIncidentTitle: string = storyResult.incident_title ? System.decryptDataWithUserKey(storyResult.incident_title, userEncryptionKey) : ''; + const decryptedIncidentSummary: string = storyResult.incident_summary ? System.decryptDataWithUserKey(storyResult.incident_summary, userEncryptionKey) : ''; - const incidentExists = actStories[actId].incidents.some( - (incident) => incident.incidentTitle === incidentTitle && incident.incidentSummary === incidentSummary + const incidentAlreadyExists: boolean = actStoriesMap[actId].incidents.some( + (existingIncident) => existingIncident.incidentTitle === decryptedIncidentTitle && existingIncident.incidentSummary === decryptedIncidentSummary ); - if (!incidentExists) { - actStories[actId].incidents.push({ - incidentTitle: incidentTitle, - incidentSummary: incidentSummary, - chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '', - chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : '' + if (!incidentAlreadyExists) { + actStoriesMap[actId].incidents.push({ + incidentTitle: decryptedIncidentTitle, + incidentSummary: decryptedIncidentSummary, + chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '', + chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : '' }); } } - if (story.plot_point_id) { - const plotTitle = story.plot_title ? System.decryptDataWithUserKey(story.plot_title, userKey) : ''; - const plotSummary = story.plot_summary ? System.decryptDataWithUserKey(story.plot_summary, userKey) : ''; + if (storyResult.plot_point_id) { + const decryptedPlotTitle: string = storyResult.plot_title ? System.decryptDataWithUserKey(storyResult.plot_title, userEncryptionKey) : ''; + const decryptedPlotSummary: string = storyResult.plot_summary ? System.decryptDataWithUserKey(storyResult.plot_summary, userEncryptionKey) : ''; - const plotPointExists = actStories[actId].plotPoints.some( - (plotPoint) => plotPoint.plotTitle === plotTitle && plotPoint.plotSummary === plotSummary + const plotPointAlreadyExists: boolean = actStoriesMap[actId].plotPoints.some( + (existingPlotPoint) => existingPlotPoint.plotTitle === decryptedPlotTitle && existingPlotPoint.plotSummary === decryptedPlotSummary ); - if (!plotPointExists) { - actStories[actId].plotPoints.push({ - plotTitle: plotTitle, - plotSummary: plotSummary, - chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '', - chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : '' + if (!plotPointAlreadyExists) { + actStoriesMap[actId].plotPoints.push({ + plotTitle: decryptedPlotTitle, + plotSummary: decryptedPlotSummary, + chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '', + chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : '' }); } } } - return Object.values(actStories); + return Object.values(actStoriesMap); } - - static getChapterContentByVersion(userId: string, chapterid: string, version: number, lang: 'fr' | 'en' = 'fr'): string { - const chapter: ContentQueryResult = ChapterRepo.fetchChapterContentByVersion(userId, chapterid, version, lang); - const userKey: string = getUserEncryptionKey(userId); - return chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : ''; + /** + * Retrieves the content of a specific chapter version. + * @param userId - The unique identifier of the user + * @param chapterId - The unique identifier of the chapter + * @param version - The version number of the content to retrieve + * @param lang - The language for error messages ('fr' or 'en') + * @returns The decrypted content string, or empty string if not found + */ + static getChapterContentByVersion(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): string { + const contentResult: ContentQueryResult = ChapterContentRepository.fetchChapterContentByVersion(userId, chapterId, version, lang); + const userEncryptionKey: string = getUserEncryptionKey(userId); + return contentResult.content ? System.decryptDataWithUserKey(contentResult.content, userEncryptionKey) : ''; } - static removeChapterInformation(userId: string, chapterInfoId: string, lang: 'fr' | 'en' = 'fr') { + /** + * Removes chapter information by its identifier. + * @param userId - The unique identifier of the user + * @param chapterInfoId - The unique identifier of the chapter information to remove + * @param lang - The language for error messages ('fr' or 'en') + * @returns True if the chapter information was removed successfully, false otherwise + */ + static removeChapterInformation(userId: string, chapterInfoId: string, lang: 'fr' | 'en' = 'fr'): boolean { return ChapterRepo.deleteChapterInformation(userId, chapterInfoId, lang); } + /** + * Converts TipTap JSON content to HTML string. + * Handles various node types including paragraphs, headings, lists, and text marks. + * @param tipTapContent - The TipTap JSON content to convert + * @returns The converted HTML string + */ static tipTapToHtml(tipTapContent: JSON): string { - interface TipTapNode { - type?: string; - text?: string; - content?: TipTapNode[]; - attrs?: Record; - marks?: Array<{ type: string; attrs?: Record }>; - } - - const escapeHtml = (text: string): string => { + const escapeHtmlCharacters = (text: string): string => { return text .replace(/&/g, '&') .replace(/ }>): string => { - if (!marks || marks.length === 0) return escapeHtml(text); + const renderTextWithMarks = (text: string, marks?: TipTapMark[]): string => { + if (!marks || marks.length === 0) return escapeHtmlCharacters(text); - let result = escapeHtml(text); - marks.forEach((mark) => { + let renderedText: string = escapeHtmlCharacters(text); + + marks.forEach((mark: TipTapMark) => { switch (mark.type) { case 'bold': - result = `${result}`; + renderedText = `${renderedText}`; break; case 'italic': - result = `${result}`; + renderedText = `${renderedText}`; break; case 'underline': - result = `${result}`; + renderedText = `${renderedText}`; break; case 'strike': - result = `${result}`; + renderedText = `${renderedText}`; break; case 'code': - result = `${result}`; + renderedText = `${renderedText}`; break; case 'link': - const href = mark.attrs?.href || '#'; - result = `${result}`; + const linkHref: string = (mark.attrs?.href as string) || '#'; + renderedText = `${renderedText}`; break; } }); - return result; + + return renderedText; }; - const renderNode = (node: TipTapNode): string => { + const renderTipTapNode = (node: TipTapNode): string => { if (!node) return ''; if (node.type === 'text') { - const textContent = node.text || '\u00A0'; - return renderMarks(textContent, node.marks); + const textContent: string = node.text || '\u00A0'; + return renderTextWithMarks(textContent, node.marks); } - const children = node.content?.map(renderNode).join('') || ''; - const textAlign = node.attrs?.textAlign ? ` style="text-align: ${node.attrs.textAlign}"` : ''; + const childrenHtml: string = node.content?.map(renderTipTapNode).join('') || ''; + const textAlignStyle: string = node.attrs?.textAlign ? ` style="text-align: ${node.attrs.textAlign}"` : ''; switch (node.type) { case 'doc': - return children; + return childrenHtml; case 'paragraph': - return `${children || '\u00A0'}

`; + return `${childrenHtml || '\u00A0'}

`; case 'heading': - const level = node.attrs?.level || 1; - return `${children}`; + const headingLevel: number = (node.attrs?.level as number) || 1; + return `${childrenHtml}`; case 'bulletList': - return `
    ${children}
`; + return `
    ${childrenHtml}
`; case 'orderedList': - return `
    ${children}
`; + return `
    ${childrenHtml}
`; case 'listItem': - return `
  • ${children}
  • `; + return `
  • ${childrenHtml}
  • `; case 'blockquote': - return `
    ${children}
    `; + return `
    ${childrenHtml}
    `; case 'codeBlock': - return `
    ${children}
    `; + return `
    ${childrenHtml}
    `; case 'hardBreak': return '
    '; case 'horizontalRule': return '
    '; default: - return children; + return childrenHtml; } }; - const contentNode = tipTapContent as unknown as TipTapNode; - return renderNode(contentNode); + const contentNode: TipTapNode = tipTapContent as unknown as TipTapNode; + return renderTipTapNode(contentNode); + } + + /** + * Retrieves all chapters with their content data for a specific book. + * @param userId - The unique identifier of the user + * @param bookId - The unique identifier of the book + * @param lang - The language for error messages ('fr' or 'en') + * @returns An array of ChapterContentData containing chapter details with content + */ + static getAllChapters(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterContentData[] { + try { + const completeBookData: CompleteBookData = Book.completeBookData(userId, bookId, lang); + return Chapter.getChaptersOrSheet(completeBookData.chapters); + } catch (error: unknown) { + return []; + } + } + + /** + * Processes book chapters to return either sheet content or chapter content. + * If only a sheet exists (order -1), returns the sheet. Otherwise, returns all positive-order chapters. + * @param bookChapters - Array of CompleteChapterContent from the book + * @returns An array of ChapterContentData with processed content + */ + static getChaptersOrSheet(bookChapters: CompleteChapterContent[]): ChapterContentData[] { + const processedChapters: ChapterContentData[] = []; + const sheetContent: CompleteChapterContent | undefined = bookChapters.find( + (chapter: CompleteChapterContent): boolean => chapter.order === -1 + ); + const regularChapter: CompleteChapterContent | undefined = bookChapters.find( + (chapter: CompleteChapterContent): boolean => chapter.order > 0 + ); + + if (sheetContent && !regularChapter) { + processedChapters.push({ + title: sheetContent.title, + chapterOrder: sheetContent.order, + content: System.htmlToText(Chapter.tipTapToHtml(JSON.parse(sheetContent.content))), + wordsCount: 0, + version: sheetContent.version || 0 + }); + } else if (regularChapter) { + for (const chapterData of bookChapters) { + if (chapterData.order < 0) continue; + processedChapters.push({ + title: chapterData.title, + chapterOrder: chapterData.order, + content: System.htmlToText(Chapter.tipTapToHtml(JSON.parse(chapterData.content))), + wordsCount: 0, + version: chapterData.version || 0 + }); + } + } + + return processedChapters; } } diff --git a/electron/database/models/Character.ts b/electron/database/models/Character.ts index 0cf9eb0..626937c 100644 --- a/electron/database/models/Character.ts +++ b/electron/database/models/Character.ts @@ -65,50 +65,81 @@ export interface CharacterAttribute { values: Attribute[]; } +export interface SyncedCharacter { + id: string; + name: string; + lastUpdate: number; + attributes: SyncedCharacterAttribute[]; +} + +export interface SyncedCharacterAttribute { + id: string; + name: string; + lastUpdate: number; +} + export default class Character { + /** + * Retrieves a list of all characters for a specific book. + * Decrypts character data using the user's encryption key. + * @param userId - The unique identifier of the user + * @param bookId - The unique identifier of the book + * @param lang - The language code for localization (defaults to 'fr') + * @returns An array of decrypted character properties + */ public static getCharacterList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): CharacterProps[] { - const userKey: string = getUserEncryptionKey(userId); - const characters: CharacterResult[] = CharacterRepo.fetchCharacters(userId, bookId, lang); - if (!characters) return []; - if (characters.length === 0) return []; - const characterList: CharacterProps[] = []; - for (const character of characters) { - characterList.push({ - id: character.character_id, - name: character.first_name ? System.decryptDataWithUserKey(character.first_name, userKey) : '', - lastName: character.last_name ? System.decryptDataWithUserKey(character.last_name, userKey) : '', - 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) : '', + const userEncryptionKey: string = getUserEncryptionKey(userId); + const encryptedCharacters: CharacterResult[] = CharacterRepo.fetchCharacters(userId, bookId, lang); + if (!encryptedCharacters) return []; + if (encryptedCharacters.length === 0) return []; + const decryptedCharacterList: CharacterProps[] = []; + for (const encryptedCharacter of encryptedCharacters) { + decryptedCharacterList.push({ + id: encryptedCharacter.character_id, + name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '', + lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '', + title: encryptedCharacter.title ? System.decryptDataWithUserKey(encryptedCharacter.title, userEncryptionKey) : '', + category: encryptedCharacter.category ? System.decryptDataWithUserKey(encryptedCharacter.category, userEncryptionKey) : '', + image: encryptedCharacter.image ? System.decryptDataWithUserKey(encryptedCharacter.image, userEncryptionKey) : '', + role: encryptedCharacter.role ? System.decryptDataWithUserKey(encryptedCharacter.role, userEncryptionKey) : '', + biography: encryptedCharacter.biography ? System.decryptDataWithUserKey(encryptedCharacter.biography, userEncryptionKey) : '', + history: encryptedCharacter.history ? System.decryptDataWithUserKey(encryptedCharacter.history, userEncryptionKey) : '', }) } - return characterList; + return decryptedCharacterList; } + /** + * Creates a new character with all its attributes for a specific book. + * Encrypts all character data before storing in the database. + * @param userId - The unique identifier of the user + * @param character - The character data to be created + * @param bookId - The unique identifier of the book + * @param lang - The language code for localization (defaults to 'fr') + * @param existingCharacterId - Optional existing character ID for updates or imports + * @returns The unique identifier of the newly created character + */ public static addNewCharacter(userId: string, character: CharacterPropsPost, bookId: string, lang: 'fr' | 'en' = 'fr', existingCharacterId?: string): string { - const userKey: string = getUserEncryptionKey(userId); + const userEncryptionKey: string = getUserEncryptionKey(userId); const characterId: string = existingCharacterId || System.createUniqueId(); - const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey); - const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userKey); - const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userKey); - const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey); - const encryptedImage: string = System.encryptDataWithUserKey(character.image, userKey); - const encryptedRole: string = System.encryptDataWithUserKey(character.role, userKey); - const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userKey); - const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userKey); + const encryptedName: string = System.encryptDataWithUserKey(character.name, userEncryptionKey); + const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userEncryptionKey); + const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userEncryptionKey); + const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey); + const encryptedImage: string = System.encryptDataWithUserKey(character.image, userEncryptionKey); + const encryptedRole: string = System.encryptDataWithUserKey(character.role, userEncryptionKey); + const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userEncryptionKey); + const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userEncryptionKey); CharacterRepo.addNewCharacter(userId, characterId, encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, bookId, lang); - const attributes: string[] = Object.keys(character); - for (const key of attributes) { - if (Array.isArray(character[key as keyof CharacterPropsPost])) { - const array = character[key as keyof CharacterPropsPost] as { name: string }[]; - if (array.length > 0) { - for (const item of array) { - const type: string = key; - const name: string = item.name; - this.addNewAttribute(characterId, userId, type, name, lang); + const characterPropertyKeys: string[] = Object.keys(character); + for (const propertyKey of characterPropertyKeys) { + if (Array.isArray(character[propertyKey as keyof CharacterPropsPost])) { + const attributeArray = character[propertyKey as keyof CharacterPropsPost] as { name: string }[]; + if (attributeArray.length > 0) { + for (const attributeItem of attributeArray) { + const attributeType: string = propertyKey; + const attributeName: string = attributeItem.name; + this.addNewAttribute(characterId, userId, attributeType, attributeName, lang); } } } @@ -116,85 +147,128 @@ export default class Character { return characterId; } + /** + * Updates an existing character's core properties. + * Encrypts all updated data before storing in the database. + * @param userId - The unique identifier of the user + * @param character - The character data with updated values + * @param lang - The language code for localization (defaults to 'fr') + * @returns True if the update was successful, false otherwise + */ static updateCharacter(userId: string, character: CharacterPropsPost, lang: 'fr' | 'en' = 'fr'): boolean { - const userKey: string = getUserEncryptionKey(userId); + const userEncryptionKey: string = getUserEncryptionKey(userId); if (!character.id) { return false; } - const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey); - const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userKey); - const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userKey); - const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey); - const encryptedImage: string = System.encryptDataWithUserKey(character.image, userKey); - const encryptedRole: string = System.encryptDataWithUserKey(character.role, userKey); - const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userKey); - const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userKey); + const encryptedName: string = System.encryptDataWithUserKey(character.name, userEncryptionKey); + const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userEncryptionKey); + const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userEncryptionKey); + const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey); + const encryptedImage: string = System.encryptDataWithUserKey(character.image, userEncryptionKey); + const encryptedRole: string = System.encryptDataWithUserKey(character.role, userEncryptionKey); + const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userEncryptionKey); + const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userEncryptionKey); return CharacterRepo.updateCharacter(userId, character.id, encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, System.timeStampInSeconds(), lang); } + /** + * Adds a new attribute to a character. + * Attributes are categorized properties like physical traits, skills, or goals. + * @param characterId - The unique identifier of the character + * @param userId - The unique identifier of the user + * @param type - The type/category of the attribute (e.g., 'physical', 'skills') + * @param name - The value/name of the attribute + * @param lang - The language code for localization (defaults to 'fr') + * @param existingAttributeId - Optional existing attribute ID for updates or imports + * @returns The unique identifier of the newly created attribute + */ static addNewAttribute(characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr', existingAttributeId?: string): string { - const userKey: string = getUserEncryptionKey(userId); + const userEncryptionKey: string = getUserEncryptionKey(userId); const attributeId: string = existingAttributeId || System.createUniqueId(); - const encryptedType: string = System.encryptDataWithUserKey(type, userKey); - const encryptedName: string = System.encryptDataWithUserKey(name, userKey); + const encryptedType: string = System.encryptDataWithUserKey(type, userEncryptionKey); + const encryptedName: string = System.encryptDataWithUserKey(name, userEncryptionKey); return CharacterRepo.insertAttribute(attributeId, characterId, userId, encryptedType, encryptedName, lang); } - static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr') { + /** + * Deletes an attribute from a character. + * @param userId - The unique identifier of the user + * @param attributeId - The unique identifier of the attribute to delete + * @param lang - The language code for localization (defaults to 'fr') + * @returns True if the deletion was successful, false otherwise + */ + static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr'): boolean { return CharacterRepo.deleteAttribute(userId, attributeId, lang); } + /** + * Retrieves all attributes for a specific character, grouped by type. + * Decrypts attribute data using the user's encryption key. + * @param characterId - The unique identifier of the character + * @param userId - The unique identifier of the user + * @param lang - The language code for localization (defaults to 'fr') + * @returns An array of character attributes grouped by type + */ static getAttributes(characterId: string, userId: string, lang: 'fr' | 'en' = 'fr'): CharacterAttribute[] { - const userKey: string = getUserEncryptionKey(userId); - const attributes: AttributeResult[] = CharacterRepo.fetchAttributes(characterId, userId, lang); - if (!attributes?.length) return []; + const userEncryptionKey: string = getUserEncryptionKey(userId); + const encryptedAttributes: AttributeResult[] = CharacterRepo.fetchAttributes(characterId, userId, lang); + if (!encryptedAttributes?.length) return []; - const groupedMap: Map = new Map(); + const attributesByType: Map = new Map(); - for (const attribute of attributes) { - const type: string = System.decryptDataWithUserKey(attribute.attribute_name, userKey); - const value: string = attribute.attribute_value ? System.decryptDataWithUserKey(attribute.attribute_value, userKey) : ''; + for (const encryptedAttribute of encryptedAttributes) { + const decryptedType: string = System.decryptDataWithUserKey(encryptedAttribute.attribute_name, userEncryptionKey); + const decryptedValue: string = encryptedAttribute.attribute_value ? System.decryptDataWithUserKey(encryptedAttribute.attribute_value, userEncryptionKey) : ''; - if (!groupedMap.has(type)) { - groupedMap.set(type, []); + if (!attributesByType.has(decryptedType)) { + attributesByType.set(decryptedType, []); } - groupedMap.get(type)!.push({ - id: attribute.attr_id, - name: value + attributesByType.get(decryptedType)!.push({ + id: encryptedAttribute.attr_id, + name: decryptedValue }); } return Array.from<[string, Attribute[]], CharacterAttribute>( - groupedMap, + attributesByType, ([type, values]: [string, Attribute[]]): CharacterAttribute => ({type, values}) ); } + /** + * Retrieves complete character data including all attributes for multiple characters. + * Used for exporting or displaying full character profiles. + * @param userId - The unique identifier of the user + * @param bookId - The unique identifier of the book + * @param characters - An array of character IDs to retrieve + * @param lang - The language code for localization (defaults to 'fr') + * @returns An array of complete character objects with all their attributes + */ static getCompleteCharacterList(userId: string, bookId: string, characters: string[], lang: 'fr' | 'en' = 'fr'): CompleteCharacterProps[] { - const characterList: CompleteCharacterResult[] = CharacterRepo.fetchCompleteCharacters(userId, bookId, characters, lang); + const encryptedCharacterList: CompleteCharacterResult[] = CharacterRepo.fetchCompleteCharacters(userId, bookId, characters, lang); - if (!characterList || characterList.length === 0) { + if (!encryptedCharacterList || encryptedCharacterList.length === 0) { return []; } - const userKey: string = getUserEncryptionKey(userId); + const userEncryptionKey: string = getUserEncryptionKey(userId); const completeCharactersMap = new Map(); - for (const character of characterList) { - if (!character.character_id) { + for (const encryptedCharacter of encryptedCharacterList) { + if (!encryptedCharacter.character_id) { continue; } - if (!completeCharactersMap.has(character.character_id)) { - const personnageObj: CompleteCharacterProps = { + if (!completeCharactersMap.has(encryptedCharacter.character_id)) { + const decryptedCharacter: CompleteCharacterProps = { id: '', - name: character.first_name ? System.decryptDataWithUserKey(character.first_name, userKey) : '', - lastName: character.last_name ? System.decryptDataWithUserKey(character.last_name, userKey) : '', - title: character.title ? System.decryptDataWithUserKey(character.title, userKey) : '', - category: character.category ? System.decryptDataWithUserKey(character.category, 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) : '', + name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '', + lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '', + title: encryptedCharacter.title ? System.decryptDataWithUserKey(encryptedCharacter.title, userEncryptionKey) : '', + category: encryptedCharacter.category ? System.decryptDataWithUserKey(encryptedCharacter.category, userEncryptionKey) : '', + role: encryptedCharacter.role ? System.decryptDataWithUserKey(encryptedCharacter.role, userEncryptionKey) : '', + biography: encryptedCharacter.biography ? System.decryptDataWithUserKey(encryptedCharacter.biography, userEncryptionKey) : '', + history: encryptedCharacter.history ? System.decryptDataWithUserKey(encryptedCharacter.history, userEncryptionKey) : '', physical: [], psychological: [], relations: [], @@ -204,36 +278,42 @@ export default class Character { goals: [], motivations: [] }; - completeCharactersMap.set(character.character_id, personnageObj); + completeCharactersMap.set(encryptedCharacter.character_id, decryptedCharacter); } - const personnage: CompleteCharacterProps | undefined = completeCharactersMap.get(character.character_id); + const characterEntry: CompleteCharacterProps | undefined = completeCharactersMap.get(encryptedCharacter.character_id); - if (!character.attribute_name || !personnage) { + if (!encryptedCharacter.attribute_name || !characterEntry) { continue; } - const decryptedName: string = System.decryptDataWithUserKey(character.attribute_name, userKey); - const decryptedValue: string = character.attribute_value ? System.decryptDataWithUserKey(character.attribute_value, userKey) : ''; + const decryptedAttributeName: string = System.decryptDataWithUserKey(encryptedCharacter.attribute_name, userEncryptionKey); + const decryptedAttributeValue: string = encryptedCharacter.attribute_value ? System.decryptDataWithUserKey(encryptedCharacter.attribute_value, userEncryptionKey) : ''; - if (Array.isArray(personnage[decryptedName])) { - personnage[decryptedName].push({ + if (Array.isArray(characterEntry[decryptedAttributeName])) { + characterEntry[decryptedAttributeName].push({ id: '', - name: decryptedValue + name: decryptedAttributeValue }); } } return Array.from(completeCharactersMap.values()); } + /** + * Generates a formatted vCard-style string representation of characters. + * Useful for AI context or text-based exports. + * @param characters - An array of complete character objects to format + * @returns A formatted string containing all character information + */ static characterVCard(characters: CompleteCharacterProps[]): string { - const charactersMap = new Map(); - let charactersDescription: string = ''; + const uniqueCharactersMap = new Map(); + let formattedCharactersDescription: string = ''; characters.forEach((character: CompleteCharacterProps): void => { - const characterKey: string = character.name || character.id || 'unknown'; + const characterIdentifier: string = character.name || character.id || 'unknown'; - if (!charactersMap.has(characterKey)) { - charactersMap.set(characterKey, { + if (!uniqueCharactersMap.has(characterIdentifier)) { + uniqueCharactersMap.set(characterIdentifier, { name: character.name, lastName: character.lastName, category: character.category, @@ -244,24 +324,24 @@ export default class Character { }); } - const characterData: CompleteCharacterProps = charactersMap.get(characterKey)!; + const aggregatedCharacterData: CompleteCharacterProps = uniqueCharactersMap.get(characterIdentifier)!; - Object.keys(character).forEach((fieldName: string): void => { - if (Array.isArray(character[fieldName])) { - if (!characterData[fieldName]) characterData[fieldName] = []; - (characterData[fieldName] as Attribute[]).push(...(character[fieldName] as Attribute[])); + Object.keys(character).forEach((propertyName: string): void => { + if (Array.isArray(character[propertyName])) { + if (!aggregatedCharacterData[propertyName]) aggregatedCharacterData[propertyName] = []; + (aggregatedCharacterData[propertyName] as Attribute[]).push(...(character[propertyName] as Attribute[])); } }); }); - charactersDescription = Array.from(charactersMap.values()).map((character: CompleteCharacterProps): string => { - const descriptionFields: string[] = []; + formattedCharactersDescription = Array.from(uniqueCharactersMap.values()).map((character: CompleteCharacterProps): string => { + const characterDescriptionLines: string[] = []; const fullName: string = [character.name, character.lastName].filter(Boolean).join(' '); - if (fullName) descriptionFields.push(`Nom : ${fullName}`); + if (fullName) characterDescriptionLines.push(`Nom : ${fullName}`); (['category', 'title', 'role', 'biography', 'history'] as const).forEach((propertyKey) => { if (character[propertyKey]) { - descriptionFields.push(`${propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1)} : ${character[propertyKey]}`); + characterDescriptionLines.push(`${propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1)} : ${character[propertyKey]}`); } }); @@ -269,13 +349,13 @@ export default class Character { const propertyValue: string | Attribute[] | undefined = character[propertyKey]; if (Array.isArray(propertyValue) && propertyValue.length > 0) { const capitalizedPropertyKey: string = propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1); - const formattedValues: string = propertyValue.map((item: Attribute) => item.name).join(', '); - descriptionFields.push(`${capitalizedPropertyKey} : ${formattedValues}`); + const formattedAttributeValues: string = propertyValue.map((attributeItem: Attribute) => attributeItem.name).join(', '); + characterDescriptionLines.push(`${capitalizedPropertyKey} : ${formattedAttributeValues}`); } }); - return descriptionFields.join('\n'); + return characterDescriptionLines.join('\n'); }).join('\n\n'); - return charactersDescription; + return formattedCharactersDescription; } } diff --git a/electron/database/models/Content.ts b/electron/database/models/Content.ts index 9d226c2..e27479d 100755 --- a/electron/database/models/Content.ts +++ b/electron/database/models/Content.ts @@ -1,3 +1,6 @@ +/** + * Represents a TipTap editor node structure. + */ export interface TiptapNode { type: string; content?: TiptapNode[]; @@ -7,43 +10,74 @@ export interface TiptapNode { }; } +/** + * Utility class for handling TipTap content conversions. + * Provides methods to convert TipTap JSON content to HTML and plain text. + */ export default class Content { + /** + * Converts TipTap raw JSON string content to plain text. + * First converts to HTML, then strips HTML tags to produce plain text. + * + * @param content - The TipTap JSON string to convert + * @returns The plain text representation of the content + */ static convertTipTapRawToText(content: string): string { - const text: string = this.convertTiptapToHTMLFromString(content); - return this.htmlToText(text); + const htmlContent: string = this.convertTiptapToHTMLFromString(content); + return this.htmlToText(htmlContent); } - static htmlToText(html: string) { + /** + * Converts HTML string to plain text by removing tags and normalizing whitespace. + * Preserves paragraph structure by converting block elements to newlines. + * + * @param html - The HTML string to convert + * @returns The plain text representation with preserved paragraph structure + */ + static htmlToText(html: string): string { return html - .replace(//gi, '\n') // Gérer les
    d'abord - .replace(/<\/?(p|h[1-6]|div)(\s+[^>]*)?>/gi, '\n') // Balises bloc - .replace(/<\/?[^>]+(>|$)/g, '') // Supprimer toutes les balises restantes - .replace(/(\n\s*){2,}/g, '\n\n') // Préserver les paragraphes - .replace(/^\s+|\s+$|(?<=\s)\s+/g, '') // Nettoyer les espaces + .replace(//gi, '\n') + .replace(/<\/?(p|h[1-6]|div)(\s+[^>]*)?>/gi, '\n') + .replace(/<\/?[^>]+(>|$)/g, '') + .replace(/(\n\s*){2,}/g, '\n\n') + .replace(/^\s+|\s+$|(?<=\s)\s+/g, '') .trim(); } + /** + * Converts a TipTap JSON string to HTML. + * Parses the JSON string and delegates to the node-based conversion method. + * + * @param jsonString - The TipTap JSON string to convert + * @returns The HTML representation, or empty string if JSON is invalid + */ static convertTiptapToHTMLFromString(jsonString: string): string { - // Convert the JSON string to an object - let jsonObject: TiptapNode; + let tiptapNode: TiptapNode; try { - jsonObject = JSON.parse(jsonString); + tiptapNode = JSON.parse(jsonString); } catch (error) { console.error('Invalid JSON string:', error); return ''; } - // Use the existing conversion function - return this.convertTiptapToHTML(jsonObject); + return this.convertTiptapToHTML(tiptapNode); } + /** + * Recursively converts a TipTap node structure to HTML. + * Handles various node types including documents, paragraphs, headings, lists, + * blockquotes, code blocks, and text with formatting attributes. + * + * @param node - The TipTap node to convert + * @returns The HTML representation of the node and its children + */ static convertTiptapToHTML(node: TiptapNode): string { let html = ''; switch (node.type) { case 'doc': if (node.content) { - node.content.forEach(childNode => { + node.content.forEach((childNode: TiptapNode) => { html += this.convertTiptapToHTML(childNode); }); } @@ -52,7 +86,7 @@ export default class Content { case 'paragraph': html += '

    '; if (node.content) { - node.content.forEach(childNode => { + node.content.forEach((childNode: TiptapNode) => { html += this.convertTiptapToHTML(childNode); }); } @@ -60,45 +94,44 @@ export default class Content { break; case 'text': - let textContent = node.text || ''; + let formattedText = node.text || ''; - // Apply attributes like bold, italic, etc. if (node.attrs) { if (node.attrs.bold) { - textContent = `${textContent}`; + formattedText = `${formattedText}`; } if (node.attrs.italic) { - textContent = `${textContent}`; + formattedText = `${formattedText}`; } if (node.attrs.underline) { - textContent = `${textContent}`; + formattedText = `${formattedText}`; } if (node.attrs.strike) { - textContent = `${textContent}`; + formattedText = `${formattedText}`; } if (node.attrs.link) { - textContent = `${textContent}`; + formattedText = `${formattedText}`; } } - html += textContent; + html += formattedText; break; case 'heading': - const level = node.attrs?.level || 1; - html += ``; + const headingLevel = node.attrs?.level || 1; + html += ``; if (node.content) { - node.content.forEach(childNode => { + node.content.forEach((childNode: TiptapNode) => { html += this.convertTiptapToHTML(childNode); }); } - html += ``; + html += ``; break; case 'bulletList': html += '

      '; if (node.content) { - node.content.forEach(childNode => { + node.content.forEach((childNode: TiptapNode) => { html += this.convertTiptapToHTML(childNode); }); } @@ -108,7 +141,7 @@ export default class Content { case 'orderedList': html += '
        '; if (node.content) { - node.content.forEach(childNode => { + node.content.forEach((childNode: TiptapNode) => { html += this.convertTiptapToHTML(childNode); }); } @@ -118,7 +151,7 @@ export default class Content { case 'listItem': html += '
      1. '; if (node.content) { - node.content.forEach(childNode => { + node.content.forEach((childNode: TiptapNode) => { html += this.convertTiptapToHTML(childNode); }); } @@ -128,7 +161,7 @@ export default class Content { case 'blockquote': html += '
        '; if (node.content) { - node.content.forEach(childNode => { + node.content.forEach((childNode: TiptapNode) => { html += this.convertTiptapToHTML(childNode); }); } @@ -138,7 +171,7 @@ export default class Content { case 'codeBlock': html += '
        ';
                         if (node.content) {
        -                    node.content.forEach(childNode => {
        +                    node.content.forEach((childNode: TiptapNode) => {
                                 html += this.convertTiptapToHTML(childNode);
                             });
                         }
        @@ -148,7 +181,7 @@ export default class Content {
                     default:
                         console.warn(`Unhandled node type: ${node.type}`);
                         if (node.content) {
        -                    node.content.forEach(childNode => {
        +                    node.content.forEach((childNode: TiptapNode) => {
                                 html += this.convertTiptapToHTML(childNode);
                             });
                         }
        diff --git a/electron/database/models/Cover.ts b/electron/database/models/Cover.ts
        new file mode 100644
        index 0000000..f568a5f
        --- /dev/null
        +++ b/electron/database/models/Cover.ts
        @@ -0,0 +1,62 @@
        +import * as fs from 'fs';
        +import * as path from 'path';
        +import { getUserEncryptionKey } from "../keyManager.js";
        +import System from "../System.js";
        +import BookRepo, { BookCoverQuery } from "../repositories/book.repository.js";
        +
        +/**
        + * Cover model class for managing book cover images.
        + * Provides methods to retrieve, decrypt, and delete cover pictures.
        + */
        +export default class Cover {
        +    /**
        +     * Retrieves and decrypts the cover picture for a specific book.
        +     * @param userId - The unique identifier of the user
        +     * @param bookId - The unique identifier of the book
        +     * @param lang - The language for error messages ('fr' or 'en')
        +     * @returns The decrypted cover image data, or an empty string if not found
        +     */
        +    public static async getCoverPicture(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): Promise {
        +        const coverQuery: BookCoverQuery = BookRepo.fetchBookCover(userId, bookId, lang);
        +        if (coverQuery) {
        +            const userEncryptionKey: string = getUserEncryptionKey(userId);
        +            return System.decryptDataWithUserKey(coverQuery.cover_image, userEncryptionKey);
        +        } else {
        +            return '';
        +        }
        +    }
        +
        +    /**
        +     * Deletes the cover picture association for a specific book.
        +     * Clears the cover image reference in the database.
        +     * @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 True if the cover was successfully deleted, false otherwise
        +     */
        +    public static async deleteCoverPicture(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): Promise {
        +        const existingCoverName: string = await Cover.getCoverPicture(userId, bookId, lang);
        +        return BookRepo.updateBookCover(bookId, '', userId, lang);
        +    }
        +
        +    /**
        +     * Retrieves and decrypts a picture file, returning it as a base64-encoded string.
        +     * @param userId - The unique identifier of the user
        +     * @param userKey - The user's encryption key for decrypting the image path
        +     * @param image - The encrypted image file path
        +     * @param lang - The language for error messages ('fr' or 'en')
        +     * @returns The base64-encoded image data, or an empty string if the image cannot be read
        +     */
        +    public static getPicture(userId: string, userKey: string, image: string, lang: 'fr' | 'en' = 'fr'): string {
        +        if (!image) return '';
        +        try {
        +            const decryptedFileName: string = System.decryptDataWithUserKey(image, userKey);
        +            const userDirectory: string = path.join('');
        +            fs.accessSync(userDirectory, fs.constants.R_OK);
        +            const fileData: Buffer = fs.readFileSync(userDirectory);
        +            return fileData.toString('base64');
        +        } catch (error: unknown) {
        +            return '';
        +        }
        +    }
        +}
        diff --git a/electron/database/models/Download.ts b/electron/database/models/Download.ts
        new file mode 100644
        index 0000000..224601b
        --- /dev/null
        +++ b/electron/database/models/Download.ts
        @@ -0,0 +1,200 @@
        +import {getUserEncryptionKey} from "../keyManager.js";
        +import System from "../System.js";
        +import {CompleteBook} from "./Book.js";
        +import BookRepo, {EritBooksTable} from "../repositories/book.repository.js";
        +import ChapterRepo, {
        +    BookChapterInfosTable,
        +    BookChaptersTable
        +} from "../repositories/chapter.repository.js";
        +import IncidentRepository, {BookIncidentsTable} from "../repositories/incident.repository.js";
        +import PlotPointRepository, {BookPlotPointsTable} from "../repositories/plotpoint.repository.js";
        +import ChapterContentRepository, {BookChapterContentTable} from "../repositories/chaptercontent.repository.js";
        +import CharacterRepo, {
        +    BookCharactersAttributesTable,
        +    BookCharactersTable
        +} from "../repositories/character.repository.js";
        +import LocationRepo, {
        +    BookLocationTable,
        +    LocationElementTable,
        +    LocationSubElementTable
        +} from "../repositories/location.repository.js";
        +import WorldRepository, {
        +    BookWorldElementsTable,
        +    BookWorldTable
        +} from "../repositories/world.repository.js";
        +import ActRepository, {BookActSummariesTable} from "../repositories/act.repository.js";
        +import GuidelineRepo, {
        +    BookAIGuideLineTable,
        +    BookGuideLineTable
        +} from "../repositories/guideline.repository.js";
        +import IssueRepository, {BookIssuesTable} from "../repositories/issue.repository.js";
        +
        +export default class Download {
        +    /**
        +     * Saves a complete book with all its associated data to the local database.
        +     * This method encrypts all sensitive data using the user's encryption key before storing.
        +     * It processes and inserts all book components including chapters, incidents, plot points,
        +     * chapter contents, chapter infos, characters, character attributes, locations, location elements,
        +     * location sub-elements, worlds, world elements, act summaries, AI guidelines, guidelines, and issues.
        +     *
        +     * @param userId - The unique identifier of the user who owns the book
        +     * @param data - The complete book data structure containing all book components to save
        +     * @param lang - The language code for localization ("fr" for French or "en" for English)
        +     * @returns A promise that resolves to true if all data was saved successfully, false otherwise
        +     */
        +    static async saveCompleteBook(userId: string, data: CompleteBook, lang: "fr" | "en"): Promise {
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +
        +        const bookData: EritBooksTable = data.eritBooks[0];
        +        const encryptedBookTitle: string = System.encryptDataWithUserKey(bookData.title, userEncryptionKey);
        +        const encryptedBookSubTitle: string | null = bookData.sub_title ? System.encryptDataWithUserKey(bookData.sub_title, userEncryptionKey) : null;
        +        const encryptedBookSummary: string | null = bookData.summary ? System.encryptDataWithUserKey(bookData.summary, userEncryptionKey) : null;
        +        const encryptedBookCoverImage: string | null = bookData.cover_image ? System.encryptDataWithUserKey(bookData.cover_image, userEncryptionKey) : null;
        +
        +        const bookInserted: boolean = BookRepo.insertSyncBook(
        +            bookData.book_id,
        +            userId,
        +            bookData.type,
        +            encryptedBookTitle,
        +            bookData.hashed_title,
        +            encryptedBookSubTitle,
        +            bookData.hashed_sub_title,
        +            encryptedBookSummary,
        +            bookData.serie_id,
        +            bookData.desired_release_date,
        +            bookData.desired_word_count,
        +            bookData.words_count,
        +            encryptedBookCoverImage,
        +            bookData.last_update,
        +            lang
        +        );
        +        if (!bookInserted) return false;
        +
        +        const chaptersInserted: boolean = data.chapters.every((chapter: BookChaptersTable): boolean => {
        +            const encryptedChapterTitle: string = System.encryptDataWithUserKey(chapter.title, userEncryptionKey);
        +            return ChapterRepo.insertSyncChapter(chapter.chapter_id, chapter.book_id, userId, encryptedChapterTitle, chapter.hashed_title, chapter.words_count, chapter.chapter_order, chapter.last_update, lang);
        +        });
        +        if (!chaptersInserted) return false;
        +
        +        const incidentsInserted: boolean = data.incidents.every((incident: BookIncidentsTable): boolean => {
        +            const encryptedIncidentTitle: string = System.encryptDataWithUserKey(incident.title, userEncryptionKey);
        +            const encryptedIncidentSummary: string | null = incident.summary ? System.encryptDataWithUserKey(incident.summary, userEncryptionKey) : null;
        +            return IncidentRepository.insertSyncIncident(incident.incident_id, userId, incident.book_id, encryptedIncidentTitle, incident.hashed_title, encryptedIncidentSummary, incident.last_update, lang);
        +        });
        +        if (!incidentsInserted) return false;
        +
        +        const plotPointsInserted: boolean = data.plotPoints.every((plotPoint: BookPlotPointsTable): boolean => {
        +            const encryptedPlotPointTitle: string = System.encryptDataWithUserKey(plotPoint.title, userEncryptionKey);
        +            const encryptedPlotPointSummary: string | null = plotPoint.summary ? System.encryptDataWithUserKey(plotPoint.summary, userEncryptionKey) : null;
        +            return PlotPointRepository.insertSyncPlotPoint(plotPoint.plot_point_id, encryptedPlotPointTitle, plotPoint.hashed_title, encryptedPlotPointSummary, plotPoint.linked_incident_id, userId, plotPoint.book_id, plotPoint.last_update, lang);
        +        });
        +        if (!plotPointsInserted) return false;
        +
        +        const chapterContentsInserted: boolean = data.chapterContents.every((chapterContent: BookChapterContentTable): boolean => {
        +            const encryptedChapterContent: string | null = chapterContent.content ? System.encryptDataWithUserKey(JSON.stringify(chapterContent.content), userEncryptionKey) : null;
        +            return ChapterContentRepository.insertSyncChapterContent(chapterContent.content_id, chapterContent.chapter_id, userId, chapterContent.version, encryptedChapterContent, chapterContent.words_count, chapterContent.time_on_it, chapterContent.last_update, lang);
        +        });
        +        if (!chapterContentsInserted) return false;
        +
        +        const chapterInfosInserted: boolean = data.chapterInfos.every((chapterInfo: BookChapterInfosTable): boolean => {
        +            const encryptedChapterSummary: string | null = chapterInfo.summary ? System.encryptDataWithUserKey(chapterInfo.summary, userEncryptionKey) : null;
        +            const encryptedChapterGoal: string | null = chapterInfo.goal ? System.encryptDataWithUserKey(chapterInfo.goal, userEncryptionKey) : null;
        +            return ChapterRepo.insertSyncChapterInfo(chapterInfo.chapter_info_id, chapterInfo.chapter_id, chapterInfo.act_id, chapterInfo.incident_id, chapterInfo.plot_point_id, chapterInfo.book_id, userId, encryptedChapterSummary, encryptedChapterGoal, chapterInfo.last_update, lang);
        +        });
        +        if (!chapterInfosInserted) return false;
        +
        +        const charactersInserted: boolean = data.characters.every((character: BookCharactersTable): boolean => {
        +            const encryptedCharacterFirstName: string = System.encryptDataWithUserKey(character.first_name, userEncryptionKey);
        +            const encryptedCharacterLastName: string | null = character.last_name ? System.encryptDataWithUserKey(character.last_name, userEncryptionKey) : null;
        +            const encryptedCharacterCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey);
        +            const encryptedCharacterTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userEncryptionKey) : null;
        +            const encryptedCharacterImage: string | null = character.image ? System.encryptDataWithUserKey(character.image, userEncryptionKey) : null;
        +            const encryptedCharacterRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userEncryptionKey) : null;
        +            const encryptedCharacterBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userEncryptionKey) : null;
        +            const encryptedCharacterHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userEncryptionKey) : null;
        +            return CharacterRepo.insertSyncCharacter(character.character_id, character.book_id, userId, encryptedCharacterFirstName, encryptedCharacterLastName, encryptedCharacterCategory, encryptedCharacterTitle, encryptedCharacterImage, encryptedCharacterRole, encryptedCharacterBiography, encryptedCharacterHistory, character.last_update, lang);
        +        });
        +        if (!charactersInserted) return false;
        +
        +        const characterAttributesInserted: boolean = data.characterAttributes.every((characterAttribute: BookCharactersAttributesTable): boolean => {
        +            const encryptedAttributeName: string = System.encryptDataWithUserKey(characterAttribute.attribute_name, userEncryptionKey);
        +            const encryptedAttributeValue: string = System.encryptDataWithUserKey(characterAttribute.attribute_value, userEncryptionKey);
        +            return CharacterRepo.insertSyncCharacterAttribute(characterAttribute.attr_id, characterAttribute.character_id, userId, encryptedAttributeName, encryptedAttributeValue, characterAttribute.last_update, lang);
        +        });
        +        if (!characterAttributesInserted) return false;
        +
        +        const locationsInserted: boolean = data.locations.every((location: BookLocationTable): boolean => {
        +            const encryptedLocationName: string = System.encryptDataWithUserKey(location.loc_name, userEncryptionKey);
        +            return LocationRepo.insertSyncLocation(location.loc_id, location.book_id, userId, encryptedLocationName, location.loc_original_name, location.last_update, lang);
        +        });
        +        if (!locationsInserted) return false;
        +
        +        const locationElementsInserted: boolean = data.locationElements.every((locationElement: LocationElementTable): boolean => {
        +            const encryptedLocationElementName: string = System.encryptDataWithUserKey(locationElement.element_name, userEncryptionKey);
        +            const encryptedLocationElementDescription: string | null = locationElement.element_description ? System.encryptDataWithUserKey(locationElement.element_description, userEncryptionKey) : null;
        +            return LocationRepo.insertSyncLocationElement(locationElement.element_id, locationElement.location, userId, encryptedLocationElementName, locationElement.original_name, encryptedLocationElementDescription, locationElement.last_update, lang);
        +        });
        +        if (!locationElementsInserted) return false;
        +
        +        const locationSubElementsInserted: boolean = data.locationSubElements.every((locationSubElement: LocationSubElementTable): boolean => {
        +            const encryptedSubElementName: string = System.encryptDataWithUserKey(locationSubElement.sub_elem_name, userEncryptionKey);
        +            const encryptedSubElementDescription: string | null = locationSubElement.sub_elem_description ? System.encryptDataWithUserKey(locationSubElement.sub_elem_description, userEncryptionKey) : null;
        +            return LocationRepo.insertSyncLocationSubElement(locationSubElement.sub_element_id, locationSubElement.element_id, userId, encryptedSubElementName, locationSubElement.original_name, encryptedSubElementDescription, locationSubElement.last_update, lang);
        +        });
        +        if (!locationSubElementsInserted) return false;
        +
        +        const worldsInserted: boolean = data.worlds.every((world: BookWorldTable): boolean => {
        +            const encryptedWorldName: string = System.encryptDataWithUserKey(world.name, userEncryptionKey);
        +            const encryptedWorldHistory: string | null = world.history ? System.encryptDataWithUserKey(world.history, userEncryptionKey) : null;
        +            const encryptedWorldPolitics: string | null = world.politics ? System.encryptDataWithUserKey(world.politics, userEncryptionKey) : null;
        +            const encryptedWorldEconomy: string | null = world.economy ? System.encryptDataWithUserKey(world.economy, userEncryptionKey) : null;
        +            const encryptedWorldReligion: string | null = world.religion ? System.encryptDataWithUserKey(world.religion, userEncryptionKey) : null;
        +            const encryptedWorldLanguages: string | null = world.languages ? System.encryptDataWithUserKey(world.languages, userEncryptionKey) : null;
        +            return WorldRepository.insertSyncWorld(world.world_id, encryptedWorldName, world.hashed_name, userId, world.book_id, encryptedWorldHistory, encryptedWorldPolitics, encryptedWorldEconomy, encryptedWorldReligion, encryptedWorldLanguages, world.last_update, lang);
        +        });
        +        if (!worldsInserted) return false;
        +
        +        const worldElementsInserted: boolean = data.worldElements.every((worldElement: BookWorldElementsTable): boolean => {
        +            const encryptedWorldElementName: string = System.encryptDataWithUserKey(worldElement.name, userEncryptionKey);
        +            const encryptedWorldElementDescription: string | null = worldElement.description ? System.encryptDataWithUserKey(worldElement.description, userEncryptionKey) : null;
        +            return WorldRepository.insertSyncWorldElement(worldElement.element_id, worldElement.world_id, userId, worldElement.element_type, encryptedWorldElementName, worldElement.original_name, encryptedWorldElementDescription, worldElement.last_update, lang);
        +        });
        +        if (!worldElementsInserted) return false;
        +
        +        const actSummariesInserted: boolean = data.actSummaries.every((actSummary: BookActSummariesTable): boolean => {
        +            const encryptedActSummary: string | null = actSummary.summary ? System.encryptDataWithUserKey(actSummary.summary, userEncryptionKey) : null;
        +            return ActRepository.insertSyncActSummary(actSummary.act_sum_id, actSummary.book_id, userId, actSummary.act_index, encryptedActSummary, actSummary.last_update, lang);
        +        });
        +        if (!actSummariesInserted) return false;
        +
        +        const aiGuidelinesInserted: boolean = data.aiGuideLine.every((aiGuideline: BookAIGuideLineTable): boolean => {
        +            const encryptedAIGlobalResume: string | null = aiGuideline.global_resume ? System.encryptDataWithUserKey(aiGuideline.global_resume, userEncryptionKey) : null;
        +            const encryptedAIThemes: string | null = aiGuideline.themes ? System.encryptDataWithUserKey(aiGuideline.themes, userEncryptionKey) : null;
        +            const encryptedAITone: string | null = aiGuideline.tone ? System.encryptDataWithUserKey(aiGuideline.tone, userEncryptionKey) : null;
        +            const encryptedAIAtmosphere: string | null = aiGuideline.atmosphere ? System.encryptDataWithUserKey(aiGuideline.atmosphere, userEncryptionKey) : null;
        +            const encryptedAICurrentResume: string | null = aiGuideline.current_resume ? System.encryptDataWithUserKey(aiGuideline.current_resume, userEncryptionKey) : null;
        +            return GuidelineRepo.insertSyncAIGuideLine(userId, aiGuideline.book_id, encryptedAIGlobalResume, encryptedAIThemes, aiGuideline.verbe_tense, aiGuideline.narrative_type, aiGuideline.langue, aiGuideline.dialogue_type, encryptedAITone, encryptedAIAtmosphere, encryptedAICurrentResume, aiGuideline.last_update, lang);
        +        });
        +        if (!aiGuidelinesInserted) return false;
        +
        +        const guidelinesInserted: boolean = data.guideLine.every((guideline: BookGuideLineTable): boolean => {
        +            const encryptedGuidelineTone: string | null = guideline.tone ? System.encryptDataWithUserKey(guideline.tone, userEncryptionKey) : null;
        +            const encryptedGuidelineAtmosphere: string | null = guideline.atmosphere ? System.encryptDataWithUserKey(guideline.atmosphere, userEncryptionKey) : null;
        +            const encryptedGuidelineWritingStyle: string | null = guideline.writing_style ? System.encryptDataWithUserKey(guideline.writing_style, userEncryptionKey) : null;
        +            const encryptedGuidelineThemes: string | null = guideline.themes ? System.encryptDataWithUserKey(guideline.themes, userEncryptionKey) : null;
        +            const encryptedGuidelineSymbolism: string | null = guideline.symbolism ? System.encryptDataWithUserKey(guideline.symbolism, userEncryptionKey) : null;
        +            const encryptedGuidelineMotifs: string | null = guideline.motifs ? System.encryptDataWithUserKey(guideline.motifs, userEncryptionKey) : null;
        +            const encryptedGuidelineNarrativeVoice: string | null = guideline.narrative_voice ? System.encryptDataWithUserKey(guideline.narrative_voice, userEncryptionKey) : null;
        +            const encryptedGuidelinePacing: string | null = guideline.pacing ? System.encryptDataWithUserKey(guideline.pacing, userEncryptionKey) : null;
        +            const encryptedGuidelineIntendedAudience: string | null = guideline.intended_audience ? System.encryptDataWithUserKey(guideline.intended_audience, userEncryptionKey) : null;
        +            const encryptedGuidelineKeyMessages: string | null = guideline.key_messages ? System.encryptDataWithUserKey(guideline.key_messages, userEncryptionKey) : null;
        +            return GuidelineRepo.insertSyncGuideLine(userId, guideline.book_id, encryptedGuidelineTone, encryptedGuidelineAtmosphere, encryptedGuidelineWritingStyle, encryptedGuidelineThemes, encryptedGuidelineSymbolism, encryptedGuidelineMotifs, encryptedGuidelineNarrativeVoice, encryptedGuidelinePacing, encryptedGuidelineIntendedAudience, encryptedGuidelineKeyMessages, guideline.last_update, lang);
        +        });
        +        if (!guidelinesInserted) return false;
        +
        +        return data.issues.every((issue: BookIssuesTable): boolean => {
        +            const encryptedIssueName: string = System.encryptDataWithUserKey(issue.name, userEncryptionKey);
        +            return IssueRepository.insertSyncIssue(issue.issue_id, userId, issue.book_id, encryptedIssueName, issue.hashed_issue_name, issue.last_update, lang);
        +        });
        +    }
        +}
        diff --git a/electron/database/models/EpubStyle.ts b/electron/database/models/EpubStyle.ts
        index cf79982..88ac97e 100755
        --- a/electron/database/models/EpubStyle.ts
        +++ b/electron/database/models/EpubStyle.ts
        @@ -1,4 +1,16 @@
        -export const mainStyle = `h1 {
        +/**
        + * Default CSS styles for EPUB export formatting.
        + *
        + * These styles are applied to the generated EPUB content to ensure
        + * consistent typography and layout across different e-readers.
        + *
        + * @remarks
        + * - h1 elements: 24px bold font with 24px text indentation
        + * - p elements: 30px text indentation, 0.7em vertical margins, justified text
        + *
        + * All styles use !important to override e-reader default styles.
        + */
        +export const mainStyle: string = `h1 {
           font-size: 24px !important;
           font-weight: bold !important;
           text-indent: 24px !important;
        diff --git a/electron/database/models/GuideLine.ts b/electron/database/models/GuideLine.ts
        new file mode 100644
        index 0000000..9fe01c2
        --- /dev/null
        +++ b/electron/database/models/GuideLine.ts
        @@ -0,0 +1,216 @@
        +import { getUserEncryptionKey } from "../keyManager.js";
        +import System from "../System.js";
        +import GuidelineRepo, { GuideLineAIQuery, GuideLineQuery } from "../repositories/guideline.repository.js";
        +
        +export interface SyncedGuideLine {
        +    lastUpdate: number;
        +}
        +
        +export interface SyncedAIGuideLine {
        +    lastUpdate: number;
        +}
        +
        +export interface GuideLineProps {
        +    tone: string;
        +    atmosphere: string;
        +    writingStyle: string;
        +    themes: string;
        +    symbolism: string;
        +    motifs: string;
        +    narrativeVoice: string;
        +    pacing: string;
        +    intendedAudience: string;
        +    keyMessages: string;
        +}
        +
        +export interface GuideLineAI {
        +    narrativeType: number | null;
        +    dialogueType: number | null;
        +    globalResume: string | null;
        +    atmosphere: string | null;
        +    verbeTense: number | null;
        +    langue: number | null;
        +    currentResume: string | null;
        +    themes: string | null;
        +}
        +
        +export default class GuideLine {
        +    /**
        +     * Retrieves and decrypts the guideline for a specific book.
        +     * @param userId - The unique identifier of the user
        +     * @param bookId - The unique identifier of the book
        +     * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
        +     * @returns The decrypted guideline properties or null if not found
        +     */
        +    public static async getGuideLine(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): Promise {
        +        const guideLineResults: GuideLineQuery[] = GuidelineRepo.fetchGuideLine(userId, bookId, lang);
        +        if (guideLineResults.length === 0) {
        +            return null;
        +        }
        +        const guideLineData: GuideLineQuery = guideLineResults[0];
        +        const encryptionKey: string = getUserEncryptionKey(userId);
        +        return {
        +            tone: guideLineData.tone ? System.decryptDataWithUserKey(guideLineData.tone, encryptionKey) : '',
        +            atmosphere: guideLineData.atmosphere ? System.decryptDataWithUserKey(guideLineData.atmosphere, encryptionKey) : '',
        +            writingStyle: guideLineData.writing_style ? System.decryptDataWithUserKey(guideLineData.writing_style, encryptionKey) : '',
        +            themes: guideLineData.themes ? System.decryptDataWithUserKey(guideLineData.themes, encryptionKey) : '',
        +            symbolism: guideLineData.symbolism ? System.decryptDataWithUserKey(guideLineData.symbolism, encryptionKey) : '',
        +            motifs: guideLineData.motifs ? System.decryptDataWithUserKey(guideLineData.motifs, encryptionKey) : '',
        +            narrativeVoice: guideLineData.narrative_voice ? System.decryptDataWithUserKey(guideLineData.narrative_voice, encryptionKey) : '',
        +            pacing: guideLineData.pacing ? System.decryptDataWithUserKey(guideLineData.pacing, encryptionKey) : '',
        +            intendedAudience: guideLineData.intended_audience ? System.decryptDataWithUserKey(guideLineData.intended_audience, encryptionKey) : '',
        +            keyMessages: guideLineData.key_messages ? System.decryptDataWithUserKey(guideLineData.key_messages, encryptionKey) : '',
        +        };
        +    }
        +
        +    /**
        +     * Updates or creates a guideline for a specific book with encrypted data.
        +     * @param userId - The unique identifier of the user
        +     * @param bookId - The unique identifier of the book
        +     * @param tone - The tone setting for the book (nullable)
        +     * @param atmosphere - The atmosphere setting for the book (nullable)
        +     * @param writingStyle - The writing style for the book (nullable)
        +     * @param themes - The themes for the book (nullable)
        +     * @param symbolism - The symbolism elements for the book (nullable)
        +     * @param motifs - The motifs for the book (nullable)
        +     * @param narrativeVoice - The narrative voice for the book (nullable)
        +     * @param pacing - The pacing setting for the book (nullable)
        +     * @param keyMessages - The key messages for the book (nullable)
        +     * @param intendedAudience - The intended audience for the book (nullable)
        +     * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
        +     * @returns True if the update was successful, false otherwise
        +     */
        +    public static async updateGuideLine(
        +        userId: string,
        +        bookId: string,
        +        tone: string | null,
        +        atmosphere: string | null,
        +        writingStyle: string | null,
        +        themes: string | null,
        +        symbolism: string | null,
        +        motifs: string | null,
        +        narrativeVoice: string | null,
        +        pacing: string | null,
        +        keyMessages: string | null,
        +        intendedAudience: string | null,
        +        lang: 'fr' | 'en' = 'fr'
        +    ): Promise {
        +        const encryptionKey: string = getUserEncryptionKey(userId);
        +        const encryptedTone: string = tone ? System.encryptDataWithUserKey(tone, encryptionKey) : '';
        +        const encryptedAtmosphere: string = atmosphere ? System.encryptDataWithUserKey(atmosphere, encryptionKey) : '';
        +        const encryptedWritingStyle: string = writingStyle ? System.encryptDataWithUserKey(writingStyle, encryptionKey) : '';
        +        const encryptedThemes: string = themes ? System.encryptDataWithUserKey(themes, encryptionKey) : '';
        +        const encryptedSymbolism: string = symbolism ? System.encryptDataWithUserKey(symbolism, encryptionKey) : '';
        +        const encryptedMotifs: string = motifs ? System.encryptDataWithUserKey(motifs, encryptionKey) : '';
        +        const encryptedNarrativeVoice: string = narrativeVoice ? System.encryptDataWithUserKey(narrativeVoice, encryptionKey) : '';
        +        const encryptedPacing: string = pacing ? System.encryptDataWithUserKey(pacing, encryptionKey) : '';
        +        const encryptedKeyMessages: string = keyMessages ? System.encryptDataWithUserKey(keyMessages, encryptionKey) : '';
        +        const encryptedIntendedAudience: string = intendedAudience ? System.encryptDataWithUserKey(intendedAudience, encryptionKey) : '';
        +
        +        return GuidelineRepo.updateGuideLine(
        +            userId,
        +            bookId,
        +            encryptedTone,
        +            encryptedAtmosphere,
        +            encryptedWritingStyle,
        +            encryptedThemes,
        +            encryptedSymbolism,
        +            encryptedMotifs,
        +            encryptedNarrativeVoice,
        +            encryptedPacing,
        +            encryptedKeyMessages,
        +            encryptedIntendedAudience,
        +            lang
        +        );
        +    }
        +
        +    /**
        +     * Retrieves and decrypts the AI guideline for a specific book.
        +     * @param userId - The unique identifier of the user
        +     * @param bookId - The unique identifier of the book
        +     * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
        +     * @returns The decrypted AI guideline data with default values if not found
        +     * @throws Error if an unexpected error occurs during retrieval
        +     */
        +    static getGuideLineAI(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): GuideLineAI {
        +        const encryptionKey: string = getUserEncryptionKey(userId);
        +        try {
        +            const aiGuideLineData: GuideLineAIQuery = GuidelineRepo.fetchGuideLineAI(userId, bookId, lang);
        +            return {
        +                narrativeType: aiGuideLineData.narrative_type,
        +                dialogueType: aiGuideLineData.dialogue_type,
        +                globalResume: aiGuideLineData.global_resume ? System.decryptDataWithUserKey(aiGuideLineData.global_resume, encryptionKey) : '',
        +                atmosphere: aiGuideLineData.atmosphere ? System.decryptDataWithUserKey(aiGuideLineData.atmosphere, encryptionKey) : '',
        +                verbeTense: aiGuideLineData.verbe_tense,
        +                themes: aiGuideLineData.themes ? System.decryptDataWithUserKey(aiGuideLineData.themes, encryptionKey) : '',
        +                currentResume: aiGuideLineData.current_resume ? System.decryptDataWithUserKey(aiGuideLineData.current_resume, encryptionKey) : '',
        +                langue: aiGuideLineData.langue
        +            };
        +        } catch (error: unknown) {
        +            if (error instanceof Error && error.message.includes('not found')) {
        +                return {
        +                    narrativeType: 0,
        +                    dialogueType: 0,
        +                    globalResume: '',
        +                    atmosphere: '',
        +                    verbeTense: 0,
        +                    themes: '',
        +                    currentResume: '',
        +                    langue: 0
        +                };
        +            }
        +            if (error instanceof Error) {
        +                throw new Error(error.message);
        +            } else {
        +                const errorMessage: string = lang === 'fr'
        +                    ? "Erreur inconnue lors de la recuperation de la ligne directrice de l'IA."
        +                    : "Unknown error while fetching AI guideline.";
        +                throw new Error(errorMessage);
        +            }
        +        }
        +    }
        +
        +    /**
        +     * Creates or updates an AI guideline for a specific book with encrypted data.
        +     * @param userId - The unique identifier of the user
        +     * @param bookId - The unique identifier of the book
        +     * @param narrativeType - The narrative type identifier
        +     * @param dialogueType - The dialogue type identifier
        +     * @param plotSummary - The plot summary text to be encrypted
        +     * @param toneAtmosphere - The tone and atmosphere description to be encrypted
        +     * @param verbTense - The verb tense identifier
        +     * @param language - The language identifier
        +     * @param themes - The themes description to be encrypted
        +     * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
        +     * @returns True if the operation was successful, false otherwise
        +     */
        +    public static setAIGuideLine(
        +        userId: string,
        +        bookId: string,
        +        narrativeType: number,
        +        dialogueType: number,
        +        plotSummary: string,
        +        toneAtmosphere: string,
        +        verbTense: number,
        +        language: number,
        +        themes: string,
        +        lang: 'fr' | 'en' = 'fr'
        +    ): boolean {
        +        const encryptionKey: string = getUserEncryptionKey(userId);
        +        const encryptedPlotSummary: string = plotSummary ? System.encryptDataWithUserKey(plotSummary, encryptionKey) : '';
        +        const encryptedToneAtmosphere: string = toneAtmosphere ? System.encryptDataWithUserKey(toneAtmosphere, encryptionKey) : '';
        +        const encryptedThemes: string = themes ? System.encryptDataWithUserKey(themes, encryptionKey) : '';
        +        return GuidelineRepo.insertAIGuideLine(
        +            userId,
        +            bookId,
        +            narrativeType,
        +            dialogueType,
        +            encryptedPlotSummary,
        +            encryptedToneAtmosphere,
        +            verbTense,
        +            language,
        +            encryptedThemes,
        +            lang
        +        );
        +    }
        +}
        diff --git a/electron/database/models/Incident.ts b/electron/database/models/Incident.ts
        new file mode 100644
        index 0000000..4db0be8
        --- /dev/null
        +++ b/electron/database/models/Incident.ts
        @@ -0,0 +1,105 @@
        +import { getUserEncryptionKey } from "../keyManager.js";
        +import System from "../System.js";
        +import { ActChapter } from "./Act.js";
        +import IncidentRepository, { IncidentQuery } from "../repositories/incident.repository.js";
        +
        +export interface IncidentStory {
        +    incidentTitle: string;
        +    incidentSummary: string;
        +    chapterSummary: string;
        +    chapterGoal: string;
        +}
        +
        +export interface SyncedIncident {
        +    id: string;
        +    name: string;
        +    lastUpdate: number;
        +}
        +
        +export interface IncidentProps {
        +    incidentId: string;
        +    title: string;
        +    summary: string;
        +    chapters?: ActChapter[];
        +}
        +
        +export default class Incident {
        +    /**
        +     * Creates a new incident for a book.
        +     * Encrypts the incident name and generates a hashed version for indexing.
        +     * @param userId - The unique identifier of the user creating the incident
        +     * @param bookId - The unique identifier of the book to add the incident to
        +     * @param name - The plain text name of the incident
        +     * @param lang - The language for error messages (defaults to 'fr')
        +     * @param existingIncidentId - Optional existing incident ID to use instead of generating a new one
        +     * @returns The unique identifier of the created incident
        +     */
        +    public static addNewIncident(
        +        userId: string,
        +        bookId: string,
        +        name: string,
        +        lang: 'fr' | 'en' = 'fr',
        +        existingIncidentId?: string
        +    ): string {
        +        const userKey: string = getUserEncryptionKey(userId);
        +        const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
        +        const hashedName: string = System.hashElement(name);
        +        const incidentId: string = existingIncidentId || System.createUniqueId();
        +        return IncidentRepository.insertNewIncident(incidentId, userId, bookId, encryptedName, hashedName, lang);
        +    }
        +
        +    /**
        +     * Retrieves all incidents for a specific book with their associated chapters.
        +     * Decrypts incident titles and summaries using the user's encryption key.
        +     * @param userId - The unique identifier of the user
        +     * @param bookId - The unique identifier of the book
        +     * @param actChapters - Array of chapters from acts to associate with incidents
        +     * @param lang - The language for error messages (defaults to 'fr')
        +     * @returns A promise resolving to an array of incident properties with decrypted data
        +     */
        +    public static async getIncitentsIncidents(
        +        userId: string,
        +        bookId: string,
        +        actChapters: ActChapter[],
        +        lang: 'fr' | 'en' = 'fr'
        +    ): Promise {
        +        const incidentQueryResults: IncidentQuery[] = IncidentRepository.fetchAllIncitentIncidents(userId, bookId, lang);
        +        const incidents: IncidentProps[] = [];
        +        const userKey: string = getUserEncryptionKey(userId);
        +
        +        if (incidentQueryResults.length > 0) {
        +            for (const incidentRecord of incidentQueryResults) {
        +                const associatedChapters: ActChapter[] = [];
        +                for (const chapter of actChapters) {
        +                    if (chapter.incidentId === incidentRecord.incident_id) {
        +                        associatedChapters.push(chapter);
        +                    }
        +                }
        +                incidents.push({
        +                    incidentId: incidentRecord.incident_id,
        +                    title: incidentRecord.title ? System.decryptDataWithUserKey(incidentRecord.title, userKey) : '',
        +                    summary: incidentRecord.summary ? System.decryptDataWithUserKey(incidentRecord.summary, userKey) : '',
        +                    chapters: associatedChapters
        +                });
        +            }
        +        }
        +        return incidents;
        +    }
        +
        +    /**
        +     * Removes an incident from a book.
        +     * @param userId - The unique identifier of the user
        +     * @param bookId - The unique identifier of the book
        +     * @param incidentId - The unique identifier of the incident to remove
        +     * @param lang - The language for error messages (defaults to 'fr')
        +     * @returns True if the incident was successfully deleted, false otherwise
        +     */
        +    public static removeIncident(
        +        userId: string,
        +        bookId: string,
        +        incidentId: string,
        +        lang: 'fr' | 'en' = 'fr'
        +    ): boolean {
        +        return IncidentRepository.deleteIncident(userId, bookId, incidentId, lang);
        +    }
        +}
        diff --git a/electron/database/models/Issue.ts b/electron/database/models/Issue.ts
        new file mode 100644
        index 0000000..ae05c93
        --- /dev/null
        +++ b/electron/database/models/Issue.ts
        @@ -0,0 +1,98 @@
        +import { getUserEncryptionKey } from "../keyManager.js";
        +import System from "../System.js";
        +import IssueRepository, { IssueQuery } from "../repositories/issue.repository.js";
        +
        +/**
        + * Represents a synced issue with its metadata.
        + */
        +export interface SyncedIssue {
        +    id: string;
        +    name: string;
        +    lastUpdate: number;
        +}
        +
        +/**
        + * Represents the basic properties of an issue.
        + */
        +export interface IssueProps {
        +    id: string;
        +    name: string;
        +}
        +
        +/**
        + * Model class for managing issues associated with books.
        + * Provides methods for CRUD operations on issues with encryption support.
        + */
        +export default class Issue {
        +    /**
        +     * Retrieves all issues associated with a specific book.
        +     * Decrypts issue names using the user's encryption key.
        +     *
        +     * @param userId - The unique identifier of the user.
        +     * @param bookId - The unique identifier of the book.
        +     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @returns A promise resolving to an array of decrypted issues.
        +     */
        +    public static async getIssuesFromBook(
        +        userId: string,
        +        bookId: string,
        +        lang: 'fr' | 'en' = 'fr'
        +    ): Promise {
        +        const issueQueryResults: IssueQuery[] = IssueRepository.fetchIssuesFromBook(userId, bookId, lang);
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +        const decryptedIssues: IssueProps[] = [];
        +
        +        if (issueQueryResults.length > 0) {
        +            for (const issueRecord of issueQueryResults) {
        +                decryptedIssues.push({
        +                    id: issueRecord.issue_id,
        +                    name: System.decryptDataWithUserKey(issueRecord.name, userEncryptionKey)
        +                });
        +            }
        +        }
        +
        +        return decryptedIssues;
        +    }
        +
        +    /**
        +     * Creates a new issue for a book.
        +     * Encrypts and hashes the issue name before storage.
        +     *
        +     * @param userId - The unique identifier of the user.
        +     * @param bookId - The unique identifier of the book.
        +     * @param name - The plain text name of the issue.
        +     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @param existingIssueId - Optional existing issue ID for syncing purposes.
        +     * @returns The unique identifier of the created issue.
        +     */
        +    public static addNewIssue(
        +        userId: string,
        +        bookId: string,
        +        name: string,
        +        lang: 'fr' | 'en' = 'fr',
        +        existingIssueId?: string
        +    ): string {
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +        const encryptedName: string = System.encryptDataWithUserKey(name, userEncryptionKey);
        +        const hashedName: string = System.hashElement(name);
        +        const issueId: string = existingIssueId || System.createUniqueId();
        +
        +        return IssueRepository.insertNewIssue(issueId, userId, bookId, encryptedName, hashedName, lang);
        +    }
        +
        +    /**
        +     * Removes an issue from the database.
        +     *
        +     * @param userId - The unique identifier of the user.
        +     * @param issueId - The unique identifier of the issue to remove.
        +     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @returns True if the issue was successfully removed, false otherwise.
        +     */
        +    public static removeIssue(
        +        userId: string,
        +        issueId: string,
        +        lang: 'fr' | 'en' = 'fr'
        +    ): boolean {
        +        return IssueRepository.deleteIssue(userId, issueId, lang);
        +    }
        +}
        diff --git a/electron/database/models/Location.ts b/electron/database/models/Location.ts
        index 4388df5..9ed8a37 100644
        --- a/electron/database/models/Location.ts
        +++ b/electron/database/models/Location.ts
        @@ -25,22 +25,42 @@ export interface LocationProps {
             elements: Element[];
         }
         
        +export interface SyncedLocation {
        +    id: string;
        +    name: string;
        +    lastUpdate: number;
        +    elements: SyncedLocationElement[];
        +}
        +
        +export interface SyncedLocationElement {
        +    id: string;
        +    name: string;
        +    lastUpdate: number;
        +    subElements: SyncedLocationSubElement[];
        +}
        +
        +export interface SyncedLocationSubElement {
        +    id: string;
        +    name: string;
        +    lastUpdate: number;
        +}
        +
         export default class Location {
             /**
        -     * Récupère toutes les locations pour un utilisateur et un livre donnés.
        -     * @param {string} userId - L'ID de l'utilisateur.
        -     * @param {string} bookId - L'ID du livre.
        -     * @returns {LocationProps[]} - Un tableau de propriétés de location.
        -     * @throws {Error} - Lance une erreur si une exception se produit lors de la récupération des locations.
        +     * Retrieves all locations for a given user and book.
        +     * @param userId - The user's unique identifier.
        +     * @param bookId - The book's unique identifier.
        +     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @returns An array of location properties with their elements and sub-elements.
              */
             static getAllLocations(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LocationProps[] {
        -        const locations: LocationQueryResult[] = LocationRepo.getLocation(userId, bookId, lang);
        -        if (!locations || locations.length === 0) return [];
        +        const locationRecords: LocationQueryResult[] = LocationRepo.getLocation(userId, bookId, lang);
        +        if (!locationRecords || locationRecords.length === 0) return [];
                 const userKey: string = getUserEncryptionKey(userId);
         
                 const locationArray: LocationProps[] = [];
         
        -        for (const record of locations) {
        +        for (const record of locationRecords) {
                     let location = locationArray.find(loc => loc.id === record.loc_id);
         
                     if (!location) {
        @@ -57,12 +77,12 @@ export default class Location {
                         let element = location.elements.find(elem => elem.id === record.element_id);
                         if (!element) {
                             const decryptedName: string = System.decryptDataWithUserKey(record.element_name, userKey);
        -                    const decryptedDesc: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : '';
        +                    const decryptedDescription: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : '';
         
                             element = {
                                 id: record.element_id,
                                 name: decryptedName,
        -                        description: decryptedDesc,
        +                        description: decryptedDescription,
                                 subElements: []
                             };
                             location.elements.push(element);
        @@ -73,13 +93,12 @@ export default class Location {
         
                             if (!subElementExists) {
                                 const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, userKey);
        -                        const decryptedDesc: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : '';
        -
        +                        const decryptedDescription: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : '';
         
                                 element.subElements.push({
                                     id: record.sub_element_id,
                                     name: decryptedName,
        -                            description: decryptedDesc
        +                            description: decryptedDescription
                                 });
                             }
                         }
        @@ -88,47 +107,81 @@ export default class Location {
                 return locationArray;
             }
         
        +    /**
        +     * Adds a new location section for a book.
        +     * @param userId - The user's unique identifier.
        +     * @param locationName - The name of the location to create.
        +     * @param bookId - The book's unique identifier.
        +     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @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 {
                 const userKey: string = getUserEncryptionKey(userId);
        -        const originalName: string = System.hashElement(locationName);
        +        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, originalName, lang);
        +        return LocationRepo.insertLocation(userId, locationId, bookId, encryptedName, hashedName, lang);
             }
         
        -    static addLocationElement(userId: string, locationId: string, elementName: string, lang: 'fr' | 'en' = 'fr', existingElementId?: string) {
        +    /**
        +     * Adds a new element to a location.
        +     * @param userId - The user's unique identifier.
        +     * @param locationId - The parent location's unique identifier.
        +     * @param elementName - The name of the element to create.
        +     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @param existingElementId - Optional existing element ID to use instead of generating a new one.
        +     * @returns The result of the insert operation.
        +     */
        +    static addLocationElement(userId: string, locationId: string, elementName: string, lang: 'fr' | 'en' = 'fr', existingElementId?: string): string {
                 const userKey: string = getUserEncryptionKey(userId);
        -        const originalName: string = System.hashElement(elementName);
        +        const hashedName: string = System.hashElement(elementName);
                 const encryptedName: string = System.encryptDataWithUserKey(elementName, userKey);
                 const elementId: string = existingElementId || System.createUniqueId();
        -        return LocationRepo.insertLocationElement(userId, elementId, locationId, encryptedName, originalName, lang)
        +        return LocationRepo.insertLocationElement(userId, elementId, locationId, encryptedName, hashedName, lang)
             }
         
        -    static addLocationSubElement(userId: string, elementId: string, subElementName: string, lang: 'fr' | 'en' = 'fr', existingSubElementId?: string) {
        +    /**
        +     * Adds a new sub-element to a location element.
        +     * @param userId - The user's unique identifier.
        +     * @param elementId - The parent element's unique identifier.
        +     * @param subElementName - The name of the sub-element to create.
        +     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @param existingSubElementId - Optional existing sub-element ID to use instead of generating a new one.
        +     * @returns The result of the insert operation.
        +     */
        +    static addLocationSubElement(userId: string, elementId: string, subElementName: string, lang: 'fr' | 'en' = 'fr', existingSubElementId?: string): string {
                 const userKey: string = getUserEncryptionKey(userId);
        -        const originalName: string = System.hashElement(subElementName);
        +        const hashedName: string = System.hashElement(subElementName);
                 const encryptedName: string = System.encryptDataWithUserKey(subElementName, userKey);
                 const subElementId: string = existingSubElementId || System.createUniqueId();
        -        return LocationRepo.insertLocationSubElement(userId, subElementId, elementId, encryptedName, originalName, lang)
        +        return LocationRepo.insertLocationSubElement(userId, subElementId, elementId, encryptedName, hashedName, lang)
             }
         
        -    static updateLocationSection(userId: string, locations: LocationProps[], lang: 'fr' | 'en' = 'fr') {
        +    /**
        +     * Updates multiple location sections along with their elements and sub-elements.
        +     * @param userId - The user's unique identifier.
        +     * @param locations - Array of location properties to update.
        +     * @param lang - The language for response messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @returns An object indicating success and a localized message.
        +     */
        +    static updateLocationSection(userId: string, locations: LocationProps[], lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } {
                 const userKey: string = getUserEncryptionKey(userId);
         
                 for (const location of locations) {
        -            const originalName: string = System.hashElement(location.name);
        -            const encryptedName: string = System.encryptDataWithUserKey(location.name, userKey);
        -            LocationRepo.updateLocationSection(userId, location.id, encryptedName, originalName, System.timeStampInSeconds(),lang)
        +            const hashedLocationName: string = System.hashElement(location.name);
        +            const encryptedLocationName: string = System.encryptDataWithUserKey(location.name, userKey);
        +            LocationRepo.updateLocationSection(userId, location.id, encryptedLocationName, hashedLocationName, System.timeStampInSeconds(), lang)
                     for (const element of location.elements) {
        -                const originalName: string = System.hashElement(element.name);
        -                const encryptedName: string = System.encryptDataWithUserKey(element.name, userKey);
        -                const encryptDescription: string = element.description ? System.encryptDataWithUserKey(element.description, userKey) : '';
        -                LocationRepo.updateLocationElement(userId, element.id, encryptedName, originalName, encryptDescription, System.timeStampInSeconds(), lang)
        +                const hashedElementName: string = System.hashElement(element.name);
        +                const encryptedElementName: string = System.encryptDataWithUserKey(element.name, userKey);
        +                const encryptedElementDescription: string = element.description ? System.encryptDataWithUserKey(element.description, userKey) : '';
        +                LocationRepo.updateLocationElement(userId, element.id, encryptedElementName, hashedElementName, encryptedElementDescription, System.timeStampInSeconds(), lang)
                         for (const subElement of element.subElements) {
        -                    const originalName: string = System.hashElement(subElement.name);
        -                    const encryptedName: string = System.encryptDataWithUserKey(subElement.name, userKey);
        -                    const encryptDescription: string = subElement.description ? System.encryptDataWithUserKey(subElement.description, userKey) : '';
        -                    LocationRepo.updateLocationSubElement(userId, subElement.id, encryptedName, originalName, encryptDescription,System.timeStampInSeconds(),lang)
        +                    const hashedSubElementName: string = System.hashElement(subElement.name);
        +                    const encryptedSubElementName: string = System.encryptDataWithUserKey(subElement.name, userKey);
        +                    const encryptedSubElementDescription: string = subElement.description ? System.encryptDataWithUserKey(subElement.description, userKey) : '';
        +                    LocationRepo.updateLocationSubElement(userId, subElement.id, encryptedSubElementName, hashedSubElementName, encryptedSubElementDescription, System.timeStampInSeconds(), lang)
                         }
                     }
                 }
        @@ -138,32 +191,61 @@ export default class Location {
                 }
             }
         
        -    static deleteLocationSection(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr') {
        +    /**
        +     * Deletes a location section and all its associated elements and sub-elements.
        +     * @param userId - The user's unique identifier.
        +     * @param locationId - The location's unique identifier to delete.
        +     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @returns The result of the delete operation.
        +     */
        +    static deleteLocationSection(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } {
                 return LocationRepo.deleteLocationSection(userId, locationId, lang);
             }
         
        -    static deleteLocationElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr') {
        +    /**
        +     * Deletes a location element and all its associated sub-elements.
        +     * @param userId - The user's unique identifier.
        +     * @param elementId - The element's unique identifier to delete.
        +     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @returns The result of the delete operation.
        +     */
        +    static deleteLocationElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } {
                 return LocationRepo.deleteLocationElement(userId, elementId, lang);
             }
         
        -    static deleteLocationSubElement(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr') {
        +    /**
        +     * Deletes a location sub-element.
        +     * @param userId - The user's unique identifier.
        +     * @param subElementId - The sub-element's unique identifier to delete.
        +     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @returns The result of the delete operation.
        +     */
        +    static deleteLocationSubElement(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } {
                 return LocationRepo.deleteLocationSubElement(userId, subElementId, lang);
             }
         
        +    /**
        +     * Retrieves location tags (elements or sub-elements) for tagging purposes.
        +     * Returns sub-elements when an element has multiple sub-elements, otherwise returns the element itself.
        +     * @param userId - The user's unique identifier.
        +     * @param bookId - The book's unique identifier.
        +     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @returns An array of sub-elements suitable for tagging.
        +     */
             static getLocationTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SubElement[] {
        -        const data: LocationElementQueryResult[] = LocationRepo.fetchLocationTags(userId, bookId, lang);
        -        if (!data || data.length === 0) return [];
        +        const tagRecords: LocationElementQueryResult[] = LocationRepo.fetchLocationTags(userId, bookId, lang);
        +        if (!tagRecords || tagRecords.length === 0) return [];
                 const userKey: string = getUserEncryptionKey(userId);
         
                 const elementCounts = new Map();
        -        data.forEach((record: LocationElementQueryResult): void => {
        +        tagRecords.forEach((record: LocationElementQueryResult): void => {
                     elementCounts.set(record.element_id, (elementCounts.get(record.element_id) || 0) + 1);
                 });
         
                 const subElements: SubElement[] = [];
                 const processedIds = new Set();
         
        -        for (const record of data) {
        +        for (const record of tagRecords) {
                     const elementCount: number = elementCounts.get(record.element_id) || 0;
         
                     if (elementCount > 1 && record.sub_element_id) {
        @@ -189,46 +271,58 @@ export default class Location {
                 return subElements;
             }
         
        +    /**
        +     * Retrieves location elements filtered by specific tag IDs.
        +     * @param userId - The user's unique identifier.
        +     * @param locations - Array of location tag IDs to filter by.
        +     * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
        +     * @returns An array of elements with their associated sub-elements.
        +     */
             static getLocationsByTags(userId: string, locations: string[], lang: 'fr' | 'en' = 'fr'): Element[] {
        -        const locationsTags: LocationByTagResult[] = LocationRepo.fetchLocationsByTags(userId, locations, lang);
        -        if (!locationsTags || locationsTags.length === 0) return [];
        +        const locationTagRecords: LocationByTagResult[] = LocationRepo.fetchLocationsByTags(userId, locations, lang);
        +        if (!locationTagRecords || locationTagRecords.length === 0) return [];
                 const userKey: string = getUserEncryptionKey(userId);
        -        const locationTags: Element[] = [];
        -        for (const record of locationsTags) {
        -            let element: Element | undefined = locationTags.find((elem: Element): boolean => elem.name === record.element_name);
        +        const locationElements: Element[] = [];
        +        for (const record of locationTagRecords) {
        +            let element: Element | undefined = locationElements.find((elem: Element): boolean => elem.name === record.element_name);
                     if (!element) {
                         const decryptedName: string = System.decryptDataWithUserKey(record.element_name, userKey);
        -                const decryptedDesc: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : '';
        +                const decryptedDescription: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : '';
                         element = {
                             id: '',
                             name: decryptedName,
        -                    description: decryptedDesc,
        +                    description: decryptedDescription,
                             subElements: []
                         };
        -                locationTags.push(element);
        +                locationElements.push(element);
                     }
                     if (record.sub_elem_name) {
                         const subElementExists: boolean = element.subElements.some(sub => sub.name === record.sub_elem_name);
                         if (!subElementExists) {
                             const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, userKey);
        -                    const decryptedDesc: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : '';
        +                    const decryptedDescription: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : '';
                             element.subElements.push({
                                 id: '',
                                 name: decryptedName,
        -                        description: decryptedDesc
        +                        description: decryptedDescription
                             });
                         }
                     }
                 }
        -        return locationTags;
        +        return locationElements;
             }
         
        +    /**
        +     * Generates a formatted description string from an array of location elements.
        +     * @param locations - Array of location elements to describe.
        +     * @returns A formatted string with location names and descriptions.
        +     */
             static locationsDescription(locations: Element[]): string {
                 return locations.map((location: Element): string => {
        -            const fields: string[] = [];
        -            if (location.name) fields.push(`Nom : ${location.name}`);
        -            if (location.description) fields.push(`Description : ${location.description}`);
        -            return fields.join('\n');
        +            const descriptionFields: string[] = [];
        +            if (location.name) descriptionFields.push(`Nom : ${location.name}`);
        +            if (location.description) descriptionFields.push(`Description : ${location.description}`);
        +            return descriptionFields.join('\n');
                 }).join('\n\n');
             }
         }
        diff --git a/electron/database/models/Model.ts b/electron/database/models/Model.ts
        index c3fa219..0be9745 100644
        --- a/electron/database/models/Model.ts
        +++ b/electron/database/models/Model.ts
        @@ -1,4 +1,11 @@
        +/**
        + * Supported OpenAI GPT model identifiers.
        + */
         export type GPTModel = "gpt-4o-mini" | "gpt-4o-turbo" | "gpt-3.5-turbo" | "gpt-4o" | "gpt-4.1" | "gpt-4.1-nano";
        +
        +/**
        + * Supported Anthropic Claude model identifiers.
        + */
         export type AnthropicModel =
             "claude-3-7-sonnet-20250219"
             | "claude-sonnet-4-20250514"
        @@ -7,6 +14,10 @@ export type AnthropicModel =
             | "claude-3-5-sonnet-20241022"
             | "claude-3-5-sonnet-20240620"
             | "claude-3-opus-20240229";
        +
        +/**
        + * Supported Google Gemini model identifiers.
        + */
         export type GeminiModel =
             | "gemini-2.0-flash-001"
             | "gemini-2.0-flash-lite-001"
        @@ -14,16 +25,30 @@ export type GeminiModel =
             | "gemini-2.5-flash-lite"
             | "gemini-2.5-pro";
         
        +/**
        + * Configuration object representing an AI model with its pricing information.
        + */
         export interface AIModelConfig {
        +    /** Unique identifier for the AI model */
             model_id: string;
        +    /** Human-readable display name for the model */
             model_name: string;
        +    /** Brand or provider of the model (e.g., Anthropic, OpenAI, Google) */
             brand: string;
        +    /** Price per input tokens in USD */
             price_token_in: number;
        +    /** Number of input tokens per price unit */
             per_quantity_in: number;
        +    /** Price per output tokens in USD */
             price_token_out: number;
        +    /** Number of output tokens per price unit */
             per_quantity_out: number;
         }
         
        +/**
        + * Array of all available AI models with their configurations and pricing.
        + * Includes models from Anthropic (Claude), Google (Gemini), and OpenAI (GPT).
        + */
         export const AIModels: AIModelConfig[] = [
             {
                 "model_id": "claude-3-5-haiku-20241022",
        @@ -250,4 +275,4 @@ export const AIModels: AIModelConfig[] = [
                 "price_token_out": 0.4,
                 "per_quantity_out": 1000000
             }
        -]
        \ No newline at end of file
        +]
        diff --git a/electron/database/models/PlotPoint.ts b/electron/database/models/PlotPoint.ts
        new file mode 100644
        index 0000000..f76a567
        --- /dev/null
        +++ b/electron/database/models/PlotPoint.ts
        @@ -0,0 +1,106 @@
        +import { getUserEncryptionKey } from "../keyManager.js";
        +import System from "../System.js";
        +import { ActChapter } from "./Act.js";
        +import PlotPointRepository, { PlotPointQuery } from "../repositories/plotpoint.repository.js";
        +
        +export interface PlotPointStory {
        +    plotTitle: string;
        +    plotSummary: string;
        +    chapterSummary: string;
        +    chapterGoal: string;
        +}
        +
        +export interface PlotPointProps {
        +    plotPointId: string,
        +    title: string,
        +    summary: string,
        +    linkedIncidentId: string | null,
        +    chapters?: ActChapter[]
        +}
        +
        +export interface SyncedPlotPoint {
        +    id: string;
        +    name: string;
        +    lastUpdate: number;
        +}
        +
        +export default class PlotPoint {
        +    /**
        +     * Retrieves all plot points for a specific book with their associated chapters.
        +     * Decrypts plot point titles and summaries using the user's encryption key.
        +     * @param userId - The unique identifier of the user
        +     * @param bookId - The unique identifier of the book
        +     * @param actChapters - Array of act chapters to associate with plot points
        +     * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
        +     * @returns A promise resolving to an array of plot point properties with their associated chapters
        +     */
        +    public static async getPlotPoints(
        +        userId: string,
        +        bookId: string,
        +        actChapters: ActChapter[],
        +        lang: 'fr' | 'en' = 'fr'
        +    ): Promise {
        +        const plotPointQueryResults: PlotPointQuery[] = PlotPointRepository.fetchAllPlotPoints(userId, bookId, lang);
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +        const plotPoints: PlotPointProps[] = [];
        +
        +        if (plotPointQueryResults.length > 0) {
        +            for (const plotPointRow of plotPointQueryResults) {
        +                const associatedChapters: ActChapter[] = [];
        +
        +                for (const chapter of actChapters) {
        +                    if (chapter.plotPointId === plotPointRow.plot_point_id) {
        +                        associatedChapters.push(chapter);
        +                    }
        +                }
        +
        +                plotPoints.push({
        +                    plotPointId: plotPointRow.plot_point_id,
        +                    title: plotPointRow.title ? System.decryptDataWithUserKey(plotPointRow.title, userEncryptionKey) : '',
        +                    summary: plotPointRow.summary ? System.decryptDataWithUserKey(plotPointRow.summary, userEncryptionKey) : '',
        +                    linkedIncidentId: plotPointRow.linked_incident_id,
        +                    chapters: associatedChapters
        +                });
        +            }
        +        }
        +
        +        return plotPoints;
        +    }
        +
        +    /**
        +     * Creates a new plot point for a book, encrypting the name before storage.
        +     * @param userId - The unique identifier of the user
        +     * @param bookId - The unique identifier of the book
        +     * @param incidentId - The identifier of the linked incident
        +     * @param name - The name/title of the plot point
        +     * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
        +     * @param existingPlotPointId - Optional existing plot point ID to use instead of generating a new one
        +     * @returns The unique identifier of the created plot point
        +     */
        +    static addNewPlotPoint(
        +        userId: string,
        +        bookId: string,
        +        incidentId: string,
        +        name: string,
        +        lang: 'fr' | 'en' = 'fr',
        +        existingPlotPointId?: string
        +    ): string {
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +        const encryptedName: string = System.encryptDataWithUserKey(name, userEncryptionKey);
        +        const hashedName: string = System.hashElement(name);
        +        const plotPointId: string = existingPlotPointId || System.createUniqueId();
        +
        +        return PlotPointRepository.insertNewPlotPoint(plotPointId, userId, bookId, encryptedName, hashedName, incidentId, lang);
        +    }
        +
        +    /**
        +     * Removes a plot point from the database.
        +     * @param userId - The unique identifier of the user
        +     * @param plotId - The unique identifier of the plot point to remove
        +     * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
        +     * @returns True if the plot point was successfully deleted, false otherwise
        +     */
        +    static removePlotPoint(userId: string, plotId: string, lang: 'fr' | 'en' = 'fr'): boolean {
        +        return PlotPointRepository.deletePlotPoint(userId, plotId, lang);
        +    }
        +}
        diff --git a/electron/database/models/Sync.ts b/electron/database/models/Sync.ts
        new file mode 100644
        index 0000000..fc0f0e2
        --- /dev/null
        +++ b/electron/database/models/Sync.ts
        @@ -0,0 +1,876 @@
        +import { getUserEncryptionKey } from "../keyManager.js";
        +import System from "../System.js";
        +import { BookSyncCompare, CompleteBook, SyncedBook } from "./Book.js";
        +import BookRepo, { EritBooksTable, SyncedBookResult } from "../repositories/book.repository.js";
        +import ChapterRepo, {
        +    BookChapterInfosTable,
        +    BookChaptersTable,
        +    SyncedChapterInfoResult,
        +    SyncedChapterResult
        +} from "../repositories/chapter.repository.js";
        +import PlotPointRepository, { BookPlotPointsTable, SyncedPlotPointResult } from "../repositories/plotpoint.repository.js";
        +import IncidentRepository, { BookIncidentsTable, SyncedIncidentResult } from "../repositories/incident.repository.js";
        +import ChapterContentRepository, {
        +    BookChapterContentTable,
        +    SyncedChapterContentResult
        +} from "../repositories/chaptercontent.repository.js";
        +import CharacterRepo, {
        +    BookCharactersAttributesTable,
        +    BookCharactersTable,
        +    SyncedCharacterAttributeResult,
        +    SyncedCharacterResult
        +} from "../repositories/character.repository.js";
        +import LocationRepo, {
        +    BookLocationTable,
        +    LocationElementTable,
        +    LocationSubElementTable,
        +    SyncedLocationElementResult,
        +    SyncedLocationResult,
        +    SyncedLocationSubElementResult
        +} from "../repositories/location.repository.js";
        +import WorldRepository, {
        +    BookWorldElementsTable,
        +    BookWorldTable,
        +    SyncedWorldElementResult,
        +    SyncedWorldResult
        +} from "../repositories/world.repository.js";
        +import ActRepository, {
        +    BookActSummariesTable,
        +    SyncedActSummaryResult
        +} from "../repositories/act.repository.js";
        +import GuidelineRepo, {
        +    BookAIGuideLineTable,
        +    BookGuideLineTable,
        +    SyncedAIGuideLineResult,
        +    SyncedGuideLineResult
        +} from "../repositories/guideline.repository.js";
        +import IssueRepository, { BookIssuesTable, SyncedIssueResult } from "../repositories/issue.repository.js";
        +import { SyncedChapter, SyncedChapterContent, SyncedChapterInfo } from "./Chapter.js";
        +import { SyncedCharacter, SyncedCharacterAttribute } from "./Character.js";
        +import { SyncedLocation, SyncedLocationElement, SyncedLocationSubElement } from "./Location.js";
        +import { SyncedWorld, SyncedWorldElement } from "./World.js";
        +import { SyncedIncident } from "./Incident.js";
        +import { SyncedPlotPoint } from "./PlotPoint.js";
        +import { SyncedIssue } from "./Issue.js";
        +import { SyncedActSummary } from "./Act.js";
        +import { SyncedAIGuideLine, SyncedGuideLine } from "./GuideLine.js";
        +
        +/**
        + * Handles synchronization operations between local database and remote server.
        + * Provides methods to fetch, compare, and sync book data including all related entities.
        + */
        +export default class Sync {
        +    /**
        +     * Retrieves a complete book with all its associated entities for synchronization.
        +     * Decrypts all encrypted fields using the user's encryption key.
        +     * @param userId - The unique identifier of the user
        +     * @param syncCompareData - Object containing IDs of entities to retrieve for sync comparison
        +     * @param lang - The language for error messages ('fr' or 'en')
        +     * @returns A promise resolving to a CompleteBook object with all decrypted data
        +     */
        +    static async getCompleteSyncBook(userId: string, syncCompareData: BookSyncCompare, lang: "fr" | "en"): Promise {
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +        const decryptedBooks: EritBooksTable[] = [];
        +        const decryptedChapters: BookChaptersTable[] = [];
        +        const decryptedPlotPoints: BookPlotPointsTable[] = [];
        +        const decryptedIncidents: BookIncidentsTable[] = [];
        +        const decryptedChapterContents: BookChapterContentTable[] = [];
        +        const decryptedChapterInfos: BookChapterInfosTable[] = [];
        +        const decryptedCharacters: BookCharactersTable[] = [];
        +        const decryptedCharacterAttributes: BookCharactersAttributesTable[] = [];
        +        const decryptedLocations: BookLocationTable[] = [];
        +        const decryptedLocationElements: LocationElementTable[] = [];
        +        const decryptedLocationSubElements: LocationSubElementTable[] = [];
        +        const decryptedWorlds: BookWorldTable[] = [];
        +        const decryptedWorldElements: BookWorldElementsTable[] = [];
        +        const decryptedActSummaries: BookActSummariesTable[] = [];
        +        const decryptedGuideLines: BookGuideLineTable[] = [];
        +        const decryptedAIGuideLines: BookAIGuideLineTable[] = [];
        +        const decryptedIssues: BookIssuesTable[] = [];
        +
        +        const actSummaryIds: string[] = syncCompareData.actSummaries;
        +        const chapterIds: string[] = syncCompareData.chapters;
        +        const plotPointIds: string[] = syncCompareData.plotPoints;
        +        const incidentIds: string[] = syncCompareData.incidents;
        +        const chapterContentIds: string[] = syncCompareData.chapterContents;
        +        const chapterInfoIds: string[] = syncCompareData.chapterInfos;
        +        const characterIds: string[] = syncCompareData.characters;
        +        const characterAttributeIds: string[] = syncCompareData.characterAttributes;
        +        const locationIds: string[] = syncCompareData.locations;
        +        const locationElementIds: string[] = syncCompareData.locationElements;
        +        const locationSubElementIds: string[] = syncCompareData.locationSubElements;
        +        const worldIds: string[] = syncCompareData.worlds;
        +        const worldElementIds: string[] = syncCompareData.worldElements;
        +        const issueIds: string[] = syncCompareData.issues;
        +
        +        if (actSummaryIds && actSummaryIds.length > 0) {
        +            for (const actSummaryId of actSummaryIds) {
        +                const actSummaryResults: BookActSummariesTable[] = await ActRepository.fetchCompleteActSummaryById(actSummaryId, lang);
        +                if (actSummaryResults.length > 0) {
        +                    const actSummaryRecord: BookActSummariesTable = actSummaryResults[0];
        +                    decryptedActSummaries.push({
        +                        ...actSummaryRecord,
        +                        summary: actSummaryRecord.summary ? System.decryptDataWithUserKey(actSummaryRecord.summary, userEncryptionKey) : null
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (chapterIds && chapterIds.length > 0) {
        +            for (const chapterId of chapterIds) {
        +                const chapterResults: BookChaptersTable[] = await ChapterRepo.fetchCompleteChapterById(chapterId, lang);
        +                if (chapterResults.length > 0) {
        +                    const chapterRecord: BookChaptersTable = chapterResults[0];
        +                    decryptedChapters.push({
        +                        ...chapterRecord,
        +                        title: System.decryptDataWithUserKey(chapterRecord.title, userEncryptionKey)
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (plotPointIds && plotPointIds.length > 0) {
        +            for (const plotPointId of plotPointIds) {
        +                const plotPointResults: BookPlotPointsTable[] = await PlotPointRepository.fetchCompletePlotPointById(plotPointId, lang);
        +                if (plotPointResults.length > 0) {
        +                    const plotPointRecord: BookPlotPointsTable = plotPointResults[0];
        +                    decryptedPlotPoints.push({
        +                        ...plotPointRecord,
        +                        title: System.decryptDataWithUserKey(plotPointRecord.title, userEncryptionKey),
        +                        summary: plotPointRecord.summary ? System.decryptDataWithUserKey(plotPointRecord.summary, userEncryptionKey) : null
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (incidentIds && incidentIds.length > 0) {
        +            for (const incidentId of incidentIds) {
        +                const incidentResults: BookIncidentsTable[] = await IncidentRepository.fetchCompleteIncidentById(incidentId, lang);
        +                if (incidentResults.length > 0) {
        +                    const incidentRecord: BookIncidentsTable = incidentResults[0];
        +                    decryptedIncidents.push({
        +                        ...incidentRecord,
        +                        title: System.decryptDataWithUserKey(incidentRecord.title, userEncryptionKey),
        +                        summary: incidentRecord.summary ? System.decryptDataWithUserKey(incidentRecord.summary, userEncryptionKey) : null
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (chapterContentIds && chapterContentIds.length > 0) {
        +            for (const chapterContentId of chapterContentIds) {
        +                const chapterContentResults: BookChapterContentTable[] = await ChapterContentRepository.fetchCompleteChapterContentById(chapterContentId, lang);
        +                if (chapterContentResults.length > 0) {
        +                    const chapterContentRecord: BookChapterContentTable = chapterContentResults[0];
        +                    decryptedChapterContents.push({
        +                        ...chapterContentRecord,
        +                        content: chapterContentRecord.content ? JSON.parse(System.decryptDataWithUserKey(chapterContentRecord.content, userEncryptionKey)) : null
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (chapterInfoIds && chapterInfoIds.length > 0) {
        +            for (const chapterInfoId of chapterInfoIds) {
        +                const chapterInfoResults: BookChapterInfosTable[] = await ChapterRepo.fetchCompleteChapterInfoById(chapterInfoId, lang);
        +                if (chapterInfoResults.length > 0) {
        +                    const chapterInfoRecord: BookChapterInfosTable = chapterInfoResults[0];
        +                    decryptedChapterInfos.push({
        +                        ...chapterInfoRecord,
        +                        summary: chapterInfoRecord.summary ? System.decryptDataWithUserKey(chapterInfoRecord.summary, userEncryptionKey) : null,
        +                        goal: chapterInfoRecord.goal ? System.decryptDataWithUserKey(chapterInfoRecord.goal, userEncryptionKey) : null
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (characterIds && characterIds.length > 0) {
        +            for (const characterId of characterIds) {
        +                const characterResults: BookCharactersTable[] = await CharacterRepo.fetchCompleteCharacterById(characterId, lang);
        +                if (characterResults.length > 0) {
        +                    const characterRecord: BookCharactersTable = characterResults[0];
        +                    decryptedCharacters.push({
        +                        ...characterRecord,
        +                        first_name: System.decryptDataWithUserKey(characterRecord.first_name, userEncryptionKey),
        +                        last_name: characterRecord.last_name ? System.decryptDataWithUserKey(characterRecord.last_name, userEncryptionKey) : null,
        +                        category: System.decryptDataWithUserKey(characterRecord.category, userEncryptionKey),
        +                        title: characterRecord.title ? System.decryptDataWithUserKey(characterRecord.title, userEncryptionKey) : null,
        +                        role: characterRecord.role ? System.decryptDataWithUserKey(characterRecord.role, userEncryptionKey) : null,
        +                        biography: characterRecord.biography ? System.decryptDataWithUserKey(characterRecord.biography, userEncryptionKey) : null,
        +                        history: characterRecord.history ? System.decryptDataWithUserKey(characterRecord.history, userEncryptionKey) : null
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (characterAttributeIds && characterAttributeIds.length > 0) {
        +            for (const characterAttributeId of characterAttributeIds) {
        +                const characterAttributeResults: BookCharactersAttributesTable[] = await CharacterRepo.fetchCompleteCharacterAttributeById(characterAttributeId, lang);
        +                if (characterAttributeResults.length > 0) {
        +                    const characterAttributeRecord: BookCharactersAttributesTable = characterAttributeResults[0];
        +                    decryptedCharacterAttributes.push({
        +                        ...characterAttributeRecord,
        +                        attribute_name: System.decryptDataWithUserKey(characterAttributeRecord.attribute_name, userEncryptionKey),
        +                        attribute_value: System.decryptDataWithUserKey(characterAttributeRecord.attribute_value, userEncryptionKey)
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (locationIds && locationIds.length > 0) {
        +            for (const locationId of locationIds) {
        +                const locationResults: BookLocationTable[] = await LocationRepo.fetchCompleteLocationById(locationId, lang);
        +                if (locationResults.length > 0) {
        +                    const locationRecord: BookLocationTable = locationResults[0];
        +                    decryptedLocations.push({
        +                        ...locationRecord,
        +                        loc_name: System.decryptDataWithUserKey(locationRecord.loc_name, userEncryptionKey)
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (locationElementIds && locationElementIds.length > 0) {
        +            for (const locationElementId of locationElementIds) {
        +                const locationElementResults: LocationElementTable[] = await LocationRepo.fetchCompleteLocationElementById(locationElementId, lang);
        +                if (locationElementResults.length > 0) {
        +                    const locationElementRecord: LocationElementTable = locationElementResults[0];
        +                    decryptedLocationElements.push({
        +                        ...locationElementRecord,
        +                        element_name: System.decryptDataWithUserKey(locationElementRecord.element_name, userEncryptionKey),
        +                        element_description: locationElementRecord.element_description ? System.decryptDataWithUserKey(locationElementRecord.element_description, userEncryptionKey) : null
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (locationSubElementIds && locationSubElementIds.length > 0) {
        +            for (const locationSubElementId of locationSubElementIds) {
        +                const locationSubElementResults: LocationSubElementTable[] = await LocationRepo.fetchCompleteLocationSubElementById(locationSubElementId, lang);
        +                if (locationSubElementResults.length > 0) {
        +                    const locationSubElementRecord: LocationSubElementTable = locationSubElementResults[0];
        +                    decryptedLocationSubElements.push({
        +                        ...locationSubElementRecord,
        +                        sub_elem_name: System.decryptDataWithUserKey(locationSubElementRecord.sub_elem_name, userEncryptionKey),
        +                        sub_elem_description: locationSubElementRecord.sub_elem_description ? System.decryptDataWithUserKey(locationSubElementRecord.sub_elem_description, userEncryptionKey) : null
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (worldIds && worldIds.length > 0) {
        +            for (const worldId of worldIds) {
        +                const worldResults: BookWorldTable[] = await WorldRepository.fetchCompleteWorldById(worldId, lang);
        +                if (worldResults.length > 0) {
        +                    const worldRecord: BookWorldTable = worldResults[0];
        +                    decryptedWorlds.push({
        +                        ...worldRecord,
        +                        name: System.decryptDataWithUserKey(worldRecord.name, userEncryptionKey),
        +                        history: worldRecord.history ? System.decryptDataWithUserKey(worldRecord.history, userEncryptionKey) : null,
        +                        politics: worldRecord.politics ? System.decryptDataWithUserKey(worldRecord.politics, userEncryptionKey) : null,
        +                        economy: worldRecord.economy ? System.decryptDataWithUserKey(worldRecord.economy, userEncryptionKey) : null,
        +                        religion: worldRecord.religion ? System.decryptDataWithUserKey(worldRecord.religion, userEncryptionKey) : null,
        +                        languages: worldRecord.languages ? System.decryptDataWithUserKey(worldRecord.languages, userEncryptionKey) : null
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (worldElementIds && worldElementIds.length > 0) {
        +            for (const worldElementId of worldElementIds) {
        +                const worldElementResults: BookWorldElementsTable[] = await WorldRepository.fetchCompleteWorldElementById(worldElementId, lang);
        +                if (worldElementResults.length > 0) {
        +                    const worldElementRecord: BookWorldElementsTable = worldElementResults[0];
        +                    decryptedWorldElements.push({
        +                        ...worldElementRecord,
        +                        name: System.decryptDataWithUserKey(worldElementRecord.name, userEncryptionKey),
        +                        description: worldElementRecord.description ? System.decryptDataWithUserKey(worldElementRecord.description, userEncryptionKey) : null
        +                    });
        +                }
        +            }
        +        }
        +
        +        if (issueIds && issueIds.length > 0) {
        +            for (const issueId of issueIds) {
        +                const issueResults: BookIssuesTable[] = await IssueRepository.fetchCompleteIssueById(issueId, lang);
        +                if (issueResults.length > 0) {
        +                    const issueRecord: BookIssuesTable = issueResults[0];
        +                    decryptedIssues.push({
        +                        ...issueRecord,
        +                        name: System.decryptDataWithUserKey(issueRecord.name, userEncryptionKey)
        +                    });
        +                }
        +            }
        +        }
        +
        +        const bookResults: EritBooksTable[] = await BookRepo.fetchCompleteBookById(syncCompareData.id, lang);
        +        if (bookResults.length > 0) {
        +            const bookRecord: EritBooksTable = bookResults[0];
        +            decryptedBooks.push({
        +                ...bookRecord,
        +                title: System.decryptDataWithUserKey(bookRecord.title, userEncryptionKey),
        +                sub_title: bookRecord.sub_title ? System.decryptDataWithUserKey(bookRecord.sub_title, userEncryptionKey) : null,
        +                summary: bookRecord.summary ? System.decryptDataWithUserKey(bookRecord.summary, userEncryptionKey) : null,
        +                cover_image: bookRecord.cover_image ? System.decryptDataWithUserKey(bookRecord.cover_image, userEncryptionKey) : null
        +            });
        +        }
        +
        +        return {
        +            eritBooks: decryptedBooks,
        +            chapters: decryptedChapters,
        +            plotPoints: decryptedPlotPoints,
        +            incidents: decryptedIncidents,
        +            chapterContents: decryptedChapterContents,
        +            chapterInfos: decryptedChapterInfos,
        +            characters: decryptedCharacters,
        +            characterAttributes: decryptedCharacterAttributes,
        +            locations: decryptedLocations,
        +            locationElements: decryptedLocationElements,
        +            locationSubElements: decryptedLocationSubElements,
        +            worlds: decryptedWorlds,
        +            worldElements: decryptedWorldElements,
        +            actSummaries: decryptedActSummaries,
        +            guideLine: decryptedGuideLines,
        +            aiGuideLine: decryptedAIGuideLines,
        +            issues: decryptedIssues
        +        };
        +    }
        +
        +    /**
        +     * Synchronizes a complete book from the server to the local client database.
        +     * Encrypts all data before storing and handles both insert and update operations.
        +     * @param userId - The unique identifier of the user
        +     * @param completeBook - The complete book data received from the server
        +     * @param lang - The language for error messages ('fr' or 'en')
        +     * @returns A promise resolving to true if sync was successful, false otherwise
        +     */
        +    static async syncBookFromServerToClient(userId: string, completeBook: CompleteBook, lang: "fr" | "en"): Promise {
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +
        +        const serverActSummaries: BookActSummariesTable[] = completeBook.actSummaries;
        +        const serverChapters: BookChaptersTable[] = completeBook.chapters;
        +        const serverPlotPoints: BookPlotPointsTable[] = completeBook.plotPoints;
        +        const serverIncidents: BookIncidentsTable[] = completeBook.incidents;
        +        const serverChapterContents: BookChapterContentTable[] = completeBook.chapterContents;
        +        const serverChapterInfos: BookChapterInfosTable[] = completeBook.chapterInfos;
        +        const serverCharacters: BookCharactersTable[] = completeBook.characters;
        +        const serverCharacterAttributes: BookCharactersAttributesTable[] = completeBook.characterAttributes;
        +        const serverLocations: BookLocationTable[] = completeBook.locations;
        +        const serverLocationElements: LocationElementTable[] = completeBook.locationElements;
        +        const serverLocationSubElements: LocationSubElementTable[] = completeBook.locationSubElements;
        +        const serverWorlds: BookWorldTable[] = completeBook.worlds;
        +        const serverWorldElements: BookWorldElementsTable[] = completeBook.worldElements;
        +        const serverIssues: BookIssuesTable[] = completeBook.issues;
        +
        +        const bookId: string = completeBook.eritBooks.length > 0 ? completeBook.eritBooks[0].book_id : '';
        +
        +        if (serverChapters && serverChapters.length > 0) {
        +            for (const serverChapter of serverChapters) {
        +                const chapterExists: boolean = ChapterRepo.isChapterExist(userId, serverChapter.chapter_id, lang);
        +                const encryptedTitle: string = System.encryptDataWithUserKey(serverChapter.title, userEncryptionKey);
        +                if (chapterExists) {
        +                    const updateSuccessful: boolean = ChapterRepo.updateChapter(userId, serverChapter.chapter_id, encryptedTitle, serverChapter.hashed_title, serverChapter.chapter_order, serverChapter.last_update, lang);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = ChapterRepo.insertSyncChapter(serverChapter.chapter_id, serverChapter.book_id, userId, encryptedTitle, serverChapter.hashed_title, serverChapter.words_count || 0, serverChapter.chapter_order, serverChapter.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverActSummaries && serverActSummaries.length > 0) {
        +            for (const serverActSummary of serverActSummaries) {
        +                const actSummaryExists: boolean = ActRepository.actSummarizeExist(userId, bookId, serverActSummary.act_index, lang);
        +                const encryptedSummary: string = System.encryptDataWithUserKey(serverActSummary.summary ? serverActSummary.summary : '', userEncryptionKey);
        +                if (actSummaryExists) {
        +                    const updateSuccessful: boolean = ActRepository.updateActSummary(userId, bookId, serverActSummary.act_index, encryptedSummary, serverActSummary.last_update, lang);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = ActRepository.insertSyncActSummary(serverActSummary.act_sum_id, userId, bookId, serverActSummary.act_index, encryptedSummary, serverActSummary.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverPlotPoints && serverPlotPoints.length > 0) {
        +            for (const serverPlotPoint of serverPlotPoints) {
        +                const encryptedTitle: string = System.encryptDataWithUserKey(serverPlotPoint.title, userEncryptionKey);
        +                const encryptedSummary: string = System.encryptDataWithUserKey(serverPlotPoint.summary ? serverPlotPoint.summary : '', userEncryptionKey);
        +                const plotPointExists: boolean = PlotPointRepository.plotPointExist(userId, bookId, serverPlotPoint.plot_point_id, lang);
        +                if (plotPointExists) {
        +                    const updateSuccessful: boolean = PlotPointRepository.updatePlotPoint(userId, bookId, serverPlotPoint.plot_point_id, encryptedTitle, serverPlotPoint.hashed_title, encryptedSummary, serverPlotPoint.last_update, lang);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    if (!serverPlotPoint.linked_incident_id) {
        +                        return false;
        +                    }
        +                    const insertSuccessful: boolean = PlotPointRepository.insertSyncPlotPoint(serverPlotPoint.plot_point_id, encryptedTitle, serverPlotPoint.hashed_title, encryptedSummary, serverPlotPoint.linked_incident_id, serverPlotPoint.author_id, bookId, serverPlotPoint.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverIncidents && serverIncidents.length > 0) {
        +            for (const serverIncident of serverIncidents) {
        +                const encryptedTitle: string = System.encryptDataWithUserKey(serverIncident.title, userEncryptionKey);
        +                const encryptedSummary: string = System.encryptDataWithUserKey(serverIncident.summary ? serverIncident.summary : '', userEncryptionKey);
        +                const incidentExists: boolean = IncidentRepository.incidentExist(userId, bookId, serverIncident.incident_id, lang);
        +                if (incidentExists) {
        +                    const updateSuccessful: boolean = IncidentRepository.updateIncident(userId, bookId, serverIncident.incident_id, encryptedTitle, serverIncident.hashed_title, encryptedSummary, serverIncident.last_update, lang);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = IncidentRepository.insertSyncIncident(serverIncident.incident_id, userId, bookId, encryptedTitle, serverIncident.hashed_title, encryptedSummary, serverIncident.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverChapterContents && serverChapterContents.length > 0) {
        +            for (const serverChapterContent of serverChapterContents) {
        +                const chapterContentExists: boolean = ChapterContentRepository.isChapterContentExist(userId, serverChapterContent.content_id, lang);
        +                const encryptedContent: string = System.encryptDataWithUserKey(serverChapterContent.content ? JSON.stringify(serverChapterContent.content) : '', userEncryptionKey);
        +                if (chapterContentExists) {
        +                    const updateSuccessful: boolean = ChapterContentRepository.updateChapterContent(userId, serverChapterContent.chapter_id, serverChapterContent.version, encryptedContent, serverChapterContent.words_count, serverChapterContent.last_update);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = ChapterContentRepository.insertSyncChapterContent(serverChapterContent.content_id, serverChapterContent.chapter_id, userId, serverChapterContent.version, encryptedContent, serverChapterContent.words_count, serverChapterContent.time_on_it, serverChapterContent.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverChapterInfos && serverChapterInfos.length > 0) {
        +            for (const serverChapterInfo of serverChapterInfos) {
        +                const chapterInfoExists: boolean = ChapterRepo.isChapterInfoExist(userId, serverChapterInfo.chapter_id, lang);
        +                const encryptedSummary: string = System.encryptDataWithUserKey(serverChapterInfo.summary ? serverChapterInfo.summary : '', userEncryptionKey);
        +                const encryptedGoal: string = System.encryptDataWithUserKey(serverChapterInfo.goal ? serverChapterInfo.goal : '', userEncryptionKey);
        +                if (chapterInfoExists) {
        +                    const updateSuccessful: boolean = ChapterRepo.updateChapterInfos(userId, serverChapterInfo.chapter_id, serverChapterInfo.act_id, bookId, serverChapterInfo.incident_id, serverChapterInfo.plot_point_id, encryptedSummary, encryptedGoal, serverChapterInfo.last_update, lang);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = ChapterRepo.insertSyncChapterInfo(serverChapterInfo.chapter_info_id, serverChapterInfo.chapter_id, serverChapterInfo.act_id, serverChapterInfo.incident_id, serverChapterInfo.plot_point_id, bookId, serverChapterInfo.author_id, encryptedSummary, encryptedGoal, serverChapterInfo.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverCharacters && serverCharacters.length > 0) {
        +            for (const serverCharacter of serverCharacters) {
        +                const characterExists: boolean = CharacterRepo.isCharacterExist(userId, serverCharacter.character_id, lang);
        +                const encryptedFirstName: string = System.encryptDataWithUserKey(serverCharacter.first_name, userEncryptionKey);
        +                const encryptedLastName: string = System.encryptDataWithUserKey(serverCharacter.last_name ? serverCharacter.last_name : '', userEncryptionKey);
        +                const encryptedCategory: string = System.encryptDataWithUserKey(serverCharacter.category, userEncryptionKey);
        +                const encryptedTitle: string = System.encryptDataWithUserKey(serverCharacter.title ? serverCharacter.title : '', userEncryptionKey);
        +                const encryptedRole: string = System.encryptDataWithUserKey(serverCharacter.role ? serverCharacter.role : '', userEncryptionKey);
        +                const encryptedImage: string = System.encryptDataWithUserKey(serverCharacter.image ? serverCharacter.image : '', userEncryptionKey);
        +                const encryptedBiography: string = System.encryptDataWithUserKey(serverCharacter.biography ? serverCharacter.biography : '', userEncryptionKey);
        +                const encryptedHistory: string = System.encryptDataWithUserKey(serverCharacter.history ? serverCharacter.history : '', userEncryptionKey);
        +                if (characterExists) {
        +                    const updateSuccessful: boolean = CharacterRepo.updateCharacter(userId, serverCharacter.character_id, encryptedFirstName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, serverCharacter.last_update);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = CharacterRepo.insertSyncCharacter(serverCharacter.character_id, bookId, userId, encryptedFirstName, encryptedLastName, encryptedCategory, encryptedTitle, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, serverCharacter.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverCharacterAttributes && serverCharacterAttributes.length > 0) {
        +            for (const serverCharacterAttribute of serverCharacterAttributes) {
        +                const characterAttributeExists: boolean = CharacterRepo.isCharacterAttributeExist(userId, serverCharacterAttribute.attr_id, lang);
        +                const encryptedAttributeName: string = System.encryptDataWithUserKey(serverCharacterAttribute.attribute_name, userEncryptionKey);
        +                const encryptedAttributeValue: string = System.encryptDataWithUserKey(serverCharacterAttribute.attribute_value, userEncryptionKey);
        +                if (characterAttributeExists) {
        +                    const updateSuccessful: boolean = CharacterRepo.updateCharacterAttribute(userId, serverCharacterAttribute.attr_id, encryptedAttributeName, encryptedAttributeValue, serverCharacterAttribute.last_update, lang);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = CharacterRepo.insertSyncCharacterAttribute(serverCharacterAttribute.attr_id, serverCharacterAttribute.character_id, userId, encryptedAttributeName, encryptedAttributeValue, serverCharacterAttribute.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverLocations && serverLocations.length > 0) {
        +            for (const serverLocation of serverLocations) {
        +                const locationExists: boolean = LocationRepo.isLocationExist(userId, serverLocation.loc_id, lang);
        +                const encryptedLocationName: string = System.encryptDataWithUserKey(serverLocation.loc_name, userEncryptionKey);
        +                if (locationExists) {
        +                    const updateSuccessful: boolean = LocationRepo.updateLocationSection(userId, serverLocation.loc_id, encryptedLocationName, serverLocation.loc_original_name, serverLocation.last_update, lang);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = LocationRepo.insertSyncLocation(serverLocation.loc_id, bookId, userId, encryptedLocationName, serverLocation.loc_original_name, serverLocation.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverLocationElements && serverLocationElements.length > 0) {
        +            for (const serverLocationElement of serverLocationElements) {
        +                const locationElementExists: boolean = LocationRepo.isLocationElementExist(userId, serverLocationElement.element_id, lang);
        +                const encryptedElementName: string = System.encryptDataWithUserKey(serverLocationElement.element_name, userEncryptionKey);
        +                const encryptedElementDescription: string = System.encryptDataWithUserKey(serverLocationElement.element_description ? serverLocationElement.element_description : '', userEncryptionKey);
        +                if (locationElementExists) {
        +                    const updateSuccessful: boolean = LocationRepo.updateLocationElement(userId, serverLocationElement.element_id, encryptedElementName, serverLocationElement.original_name, encryptedElementDescription, serverLocationElement.last_update, lang);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = LocationRepo.insertSyncLocationElement(serverLocationElement.element_id, serverLocationElement.location, userId, encryptedElementName, serverLocationElement.original_name, encryptedElementDescription, serverLocationElement.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverLocationSubElements && serverLocationSubElements.length > 0) {
        +            for (const serverLocationSubElement of serverLocationSubElements) {
        +                const locationSubElementExists: boolean = LocationRepo.isLocationSubElementExist(userId, serverLocationSubElement.sub_element_id, lang);
        +                const encryptedSubElementName: string = System.encryptDataWithUserKey(serverLocationSubElement.sub_elem_name, userEncryptionKey);
        +                const encryptedSubElementDescription: string = System.encryptDataWithUserKey(serverLocationSubElement.sub_elem_description ? serverLocationSubElement.sub_elem_description : '', userEncryptionKey);
        +                if (locationSubElementExists) {
        +                    const updateSuccessful: boolean = LocationRepo.updateLocationSubElement(userId, serverLocationSubElement.sub_element_id, encryptedSubElementName, serverLocationSubElement.original_name, encryptedSubElementDescription, serverLocationSubElement.last_update, lang);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = LocationRepo.insertSyncLocationSubElement(serverLocationSubElement.sub_element_id, serverLocationSubElement.element_id, userId, encryptedSubElementName, serverLocationSubElement.original_name, encryptedSubElementDescription, serverLocationSubElement.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverWorlds && serverWorlds.length > 0) {
        +            for (const serverWorld of serverWorlds) {
        +                const worldExists: boolean = WorldRepository.worldExist(userId, bookId, serverWorld.world_id, lang);
        +                const encryptedName: string = System.encryptDataWithUserKey(serverWorld.name, userEncryptionKey);
        +                const encryptedHistory: string = System.encryptDataWithUserKey(serverWorld.history ? serverWorld.history : '', userEncryptionKey);
        +                const encryptedPolitics: string = System.encryptDataWithUserKey(serverWorld.politics ? serverWorld.politics : '', userEncryptionKey);
        +                const encryptedEconomy: string = System.encryptDataWithUserKey(serverWorld.economy ? serverWorld.economy : '', userEncryptionKey);
        +                const encryptedReligion: string = System.encryptDataWithUserKey(serverWorld.religion ? serverWorld.religion : '', userEncryptionKey);
        +                const encryptedLanguages: string = System.encryptDataWithUserKey(serverWorld.languages ? serverWorld.languages : '', userEncryptionKey);
        +                if (worldExists) {
        +                    const updateSuccessful: boolean = WorldRepository.updateWorld(userId, serverWorld.world_id, encryptedName, serverWorld.hashed_name, encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, serverWorld.last_update, lang);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = WorldRepository.insertSyncWorld(serverWorld.world_id, encryptedName, serverWorld.hashed_name, userId, bookId, encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, serverWorld.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverWorldElements && serverWorldElements.length > 0) {
        +            for (const serverWorldElement of serverWorldElements) {
        +                const worldElementExists: boolean = WorldRepository.worldElementExist(userId, serverWorldElement.world_id, serverWorldElement.element_id, lang);
        +                const encryptedName: string = System.encryptDataWithUserKey(serverWorldElement.name, userEncryptionKey);
        +                const encryptedDescription: string = System.encryptDataWithUserKey(serverWorldElement.description ? serverWorldElement.description : '', userEncryptionKey);
        +                if (worldElementExists) {
        +                    const updateSuccessful: boolean = WorldRepository.updateWorldElement(userId, serverWorldElement.element_id, encryptedName, encryptedDescription, serverWorldElement.last_update, lang);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = WorldRepository.insertSyncWorldElement(serverWorldElement.element_id, serverWorldElement.world_id, userId, serverWorldElement.element_type, encryptedName, serverWorldElement.original_name, encryptedDescription, serverWorldElement.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        if (serverIssues && serverIssues.length > 0) {
        +            for (const serverIssue of serverIssues) {
        +                const issueExists: boolean = IssueRepository.issueExist(userId, bookId, serverIssue.issue_id, lang);
        +                const encryptedName: string = System.encryptDataWithUserKey(serverIssue.name, userEncryptionKey);
        +                if (issueExists) {
        +                    const updateSuccessful: boolean = IssueRepository.updateIssue(userId, bookId, serverIssue.issue_id, encryptedName, serverIssue.hashed_issue_name, serverIssue.last_update, lang);
        +                    if (!updateSuccessful) {
        +                        return false;
        +                    }
        +                } else {
        +                    const insertSuccessful: boolean = IssueRepository.insertSyncIssue(serverIssue.issue_id, userId, bookId, encryptedName, serverIssue.hashed_issue_name, serverIssue.last_update, lang);
        +                    if (!insertSuccessful) {
        +                        return false;
        +                    }
        +                }
        +            }
        +        }
        +
        +        return true;
        +    }
        +
        +    /**
        +     * Retrieves all synced books for a user with their complete hierarchical data structure.
        +     * Fetches all related entities (chapters, characters, locations, etc.) and organizes them by book.
        +     * @param userId - The unique identifier of the user
        +     * @param lang - The language for error messages ('fr' or 'en')
        +     * @returns A promise resolving to an array of SyncedBook objects with decrypted data
        +     */
        +    static async getSyncedBooks(userId: string, lang: 'fr' | 'en'): Promise {
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +
        +        const [
        +            allBooks,
        +            allChapters,
        +            allChapterContents,
        +            allChapterInfos,
        +            allCharacters,
        +            allCharacterAttributes,
        +            allLocations,
        +            allLocationElements,
        +            allLocationSubElements,
        +            allWorlds,
        +            allWorldElements,
        +            allIncidents,
        +            allPlotPoints,
        +            allIssues,
        +            allActSummaries,
        +            allGuidelines,
        +            allAIGuidelines
        +        ]: [
        +            SyncedBookResult[],
        +            SyncedChapterResult[],
        +            SyncedChapterContentResult[],
        +            SyncedChapterInfoResult[],
        +            SyncedCharacterResult[],
        +            SyncedCharacterAttributeResult[],
        +            SyncedLocationResult[],
        +            SyncedLocationElementResult[],
        +            SyncedLocationSubElementResult[],
        +            SyncedWorldResult[],
        +            SyncedWorldElementResult[],
        +            SyncedIncidentResult[],
        +            SyncedPlotPointResult[],
        +            SyncedIssueResult[],
        +            SyncedActSummaryResult[],
        +            SyncedGuideLineResult[],
        +            SyncedAIGuideLineResult[]
        +        ] = await Promise.all([
        +            BookRepo.fetchSyncedBooks(userId, lang),
        +            ChapterRepo.fetchSyncedChapters(userId, lang),
        +            ChapterContentRepository.fetchSyncedChapterContents(userId, lang),
        +            ChapterRepo.fetchSyncedChapterInfos(userId, lang),
        +            CharacterRepo.fetchSyncedCharacters(userId, lang),
        +            CharacterRepo.fetchSyncedCharacterAttributes(userId, lang),
        +            LocationRepo.fetchSyncedLocations(userId, lang),
        +            LocationRepo.fetchSyncedLocationElements(userId, lang),
        +            LocationRepo.fetchSyncedLocationSubElements(userId, lang),
        +            WorldRepository.fetchSyncedWorlds(userId, lang),
        +            WorldRepository.fetchSyncedWorldElements(userId, lang),
        +            IncidentRepository.fetchSyncedIncidents(userId, lang),
        +            PlotPointRepository.fetchSyncedPlotPoints(userId, lang),
        +            IssueRepository.fetchSyncedIssues(userId, lang),
        +            ActRepository.fetchSyncedActSummaries(userId, lang),
        +            GuidelineRepo.fetchSyncedAIGuideLine(userId, lang),
        +            GuidelineRepo.fetchSyncedGuideLine(userId, lang)
        +        ]);
        +
        +        return allBooks.map((bookRecord: SyncedBookResult): SyncedBook => {
        +            const currentBookId: string = bookRecord.book_id;
        +
        +            const bookChapters: SyncedChapter[] = allChapters
        +                .filter((chapterRecord: SyncedChapterResult): boolean => chapterRecord.book_id === currentBookId)
        +                .map((chapterRecord: SyncedChapterResult): SyncedChapter => {
        +                    const currentChapterId: string = chapterRecord.chapter_id;
        +
        +                    const chapterContents: SyncedChapterContent[] = allChapterContents
        +                        .filter((contentRecord: SyncedChapterContentResult): boolean => contentRecord.chapter_id === currentChapterId)
        +                        .map((contentRecord: SyncedChapterContentResult): SyncedChapterContent => ({
        +                            id: contentRecord.content_id,
        +                            lastUpdate: contentRecord.last_update
        +                        }));
        +
        +                    const chapterInfoRecord: SyncedChapterInfoResult | undefined = allChapterInfos.find((infoRecord: SyncedChapterInfoResult): boolean => infoRecord.chapter_id === currentChapterId);
        +                    const chapterInfo: SyncedChapterInfo | null = chapterInfoRecord ? {
        +                        id: chapterInfoRecord.chapter_info_id,
        +                        lastUpdate: chapterInfoRecord.last_update
        +                    } : null;
        +
        +                    return {
        +                        id: currentChapterId,
        +                        name: System.decryptDataWithUserKey(chapterRecord.title, userEncryptionKey),
        +                        lastUpdate: chapterRecord.last_update,
        +                        contents: chapterContents,
        +                        info: chapterInfo
        +                    };
        +                });
        +
        +            const bookCharacters: SyncedCharacter[] = allCharacters
        +                .filter((characterRecord: SyncedCharacterResult): boolean => characterRecord.book_id === currentBookId)
        +                .map((characterRecord: SyncedCharacterResult): SyncedCharacter => {
        +                    const currentCharacterId: string = characterRecord.character_id;
        +
        +                    const characterAttributes: SyncedCharacterAttribute[] = allCharacterAttributes
        +                        .filter((attributeRecord: SyncedCharacterAttributeResult): boolean => attributeRecord.character_id === currentCharacterId)
        +                        .map((attributeRecord: SyncedCharacterAttributeResult): SyncedCharacterAttribute => ({
        +                            id: attributeRecord.attr_id,
        +                            name: System.decryptDataWithUserKey(attributeRecord.attribute_name, userEncryptionKey),
        +                            lastUpdate: attributeRecord.last_update
        +                        }));
        +
        +                    return {
        +                        id: currentCharacterId,
        +                        name: System.decryptDataWithUserKey(characterRecord.first_name, userEncryptionKey),
        +                        lastUpdate: characterRecord.last_update,
        +                        attributes: characterAttributes
        +                    };
        +                });
        +
        +            const bookLocations: SyncedLocation[] = allLocations
        +                .filter((locationRecord: SyncedLocationResult): boolean => locationRecord.book_id === currentBookId)
        +                .map((locationRecord: SyncedLocationResult): SyncedLocation => {
        +                    const currentLocationId: string = locationRecord.loc_id;
        +
        +                    const locationElements: SyncedLocationElement[] = allLocationElements
        +                        .filter((elementRecord: SyncedLocationElementResult): boolean => elementRecord.location === currentLocationId)
        +                        .map((elementRecord: SyncedLocationElementResult): SyncedLocationElement => {
        +                            const currentElementId: string = elementRecord.element_id;
        +
        +                            const locationSubElements: SyncedLocationSubElement[] = allLocationSubElements
        +                                .filter((subElementRecord: SyncedLocationSubElementResult): boolean => subElementRecord.element_id === currentElementId)
        +                                .map((subElementRecord: SyncedLocationSubElementResult): SyncedLocationSubElement => ({
        +                                    id: subElementRecord.sub_element_id,
        +                                    name: System.decryptDataWithUserKey(subElementRecord.sub_elem_name, userEncryptionKey),
        +                                    lastUpdate: subElementRecord.last_update
        +                                }));
        +
        +                            return {
        +                                id: currentElementId,
        +                                name: System.decryptDataWithUserKey(elementRecord.element_name, userEncryptionKey),
        +                                lastUpdate: elementRecord.last_update,
        +                                subElements: locationSubElements
        +                            };
        +                        });
        +
        +                    return {
        +                        id: currentLocationId,
        +                        name: System.decryptDataWithUserKey(locationRecord.loc_name, userEncryptionKey),
        +                        lastUpdate: locationRecord.last_update,
        +                        elements: locationElements
        +                    };
        +                });
        +
        +            const bookWorlds: SyncedWorld[] = allWorlds
        +                .filter((worldRecord: SyncedWorldResult): boolean => worldRecord.book_id === currentBookId)
        +                .map((worldRecord: SyncedWorldResult): SyncedWorld => {
        +                    const currentWorldId: string = worldRecord.world_id;
        +
        +                    const worldElements: SyncedWorldElement[] = allWorldElements
        +                        .filter((worldElementRecord: SyncedWorldElementResult): boolean => worldElementRecord.world_id === currentWorldId)
        +                        .map((worldElementRecord: SyncedWorldElementResult): SyncedWorldElement => ({
        +                            id: worldElementRecord.element_id,
        +                            name: System.decryptDataWithUserKey(worldElementRecord.name, userEncryptionKey),
        +                            lastUpdate: worldElementRecord.last_update
        +                        }));
        +
        +                    return {
        +                        id: currentWorldId,
        +                        name: System.decryptDataWithUserKey(worldRecord.name, userEncryptionKey),
        +                        lastUpdate: worldRecord.last_update,
        +                        elements: worldElements
        +                    };
        +                });
        +
        +            const bookIncidents: SyncedIncident[] = allIncidents
        +                .filter((incidentRecord: SyncedIncidentResult): boolean => incidentRecord.book_id === currentBookId)
        +                .map((incidentRecord: SyncedIncidentResult): SyncedIncident => ({
        +                    id: incidentRecord.incident_id,
        +                    name: System.decryptDataWithUserKey(incidentRecord.title, userEncryptionKey),
        +                    lastUpdate: incidentRecord.last_update
        +                }));
        +
        +            const bookPlotPoints: SyncedPlotPoint[] = allPlotPoints
        +                .filter((plotPointRecord: SyncedPlotPointResult): boolean => plotPointRecord.book_id === currentBookId)
        +                .map((plotPointRecord: SyncedPlotPointResult): SyncedPlotPoint => ({
        +                    id: plotPointRecord.plot_point_id,
        +                    name: System.decryptDataWithUserKey(plotPointRecord.title, userEncryptionKey),
        +                    lastUpdate: plotPointRecord.last_update
        +                }));
        +
        +            const bookIssues: SyncedIssue[] = allIssues
        +                .filter((issueRecord: SyncedIssueResult): boolean => issueRecord.book_id === currentBookId)
        +                .map((issueRecord: SyncedIssueResult): SyncedIssue => ({
        +                    id: issueRecord.issue_id,
        +                    name: System.decryptDataWithUserKey(issueRecord.name, userEncryptionKey),
        +                    lastUpdate: issueRecord.last_update
        +                }));
        +
        +            const bookActSummaries: SyncedActSummary[] = allActSummaries
        +                .filter((actSummaryRecord: SyncedActSummaryResult): boolean => actSummaryRecord.book_id === currentBookId)
        +                .map((actSummaryRecord: SyncedActSummaryResult): SyncedActSummary => ({
        +                    id: actSummaryRecord.act_sum_id,
        +                    lastUpdate: actSummaryRecord.last_update
        +                }));
        +
        +            const guidelineRecord: SyncedGuideLineResult | undefined = allGuidelines.find((guidelineItem: SyncedGuideLineResult): boolean => guidelineItem.book_id === currentBookId);
        +            const bookGuideLine: SyncedGuideLine | null = guidelineRecord ? {
        +                lastUpdate: guidelineRecord.last_update
        +            } : null;
        +
        +            const aiGuidelineRecord: SyncedAIGuideLineResult | undefined = allAIGuidelines.find((aiGuidelineItem: SyncedAIGuideLineResult): boolean => aiGuidelineItem.book_id === currentBookId);
        +            const bookAIGuideLine: SyncedAIGuideLine | null = aiGuidelineRecord ? {
        +                lastUpdate: aiGuidelineRecord.last_update
        +            } : null;
        +
        +            return {
        +                id: currentBookId,
        +                type: bookRecord.type,
        +                title: System.decryptDataWithUserKey(bookRecord.title, userEncryptionKey),
        +                subTitle: bookRecord.sub_title ? System.decryptDataWithUserKey(bookRecord.sub_title, userEncryptionKey) : null,
        +                lastUpdate: bookRecord.last_update,
        +                chapters: bookChapters,
        +                characters: bookCharacters,
        +                locations: bookLocations,
        +                worlds: bookWorlds,
        +                incidents: bookIncidents,
        +                plotPoints: bookPlotPoints,
        +                issues: bookIssues,
        +                actSummaries: bookActSummaries,
        +                guideLine: bookGuideLine,
        +                aiGuideLine: bookAIGuideLine
        +            };
        +        });
        +    }
        +}
        diff --git a/electron/database/models/Upload.ts b/electron/database/models/Upload.ts
        new file mode 100644
        index 0000000..c62ec49
        --- /dev/null
        +++ b/electron/database/models/Upload.ts
        @@ -0,0 +1,257 @@
        +import { getUserEncryptionKey } from "../keyManager.js";
        +import System from "../System.js";
        +import { CompleteBook } from "./Book.js";
        +import BookRepo, { EritBooksTable } from "../repositories/book.repository.js";
        +import ActRepository, { BookActSummariesTable } from "../repositories/act.repository.js";
        +import GuidelineRepo, { BookAIGuideLineTable, BookGuideLineTable } from "../repositories/guideline.repository.js";
        +import ChapterRepo, {
        +    BookChapterInfosTable,
        +    BookChaptersTable
        +} from "../repositories/chapter.repository.js";
        +import CharacterRepo, {
        +    BookCharactersAttributesTable,
        +    BookCharactersTable
        +} from "../repositories/character.repository.js";
        +import IncidentRepository, { BookIncidentsTable } from "../repositories/incident.repository.js";
        +import IssueRepository, { BookIssuesTable } from "../repositories/issue.repository.js";
        +import LocationRepo, {
        +    BookLocationTable,
        +    LocationElementTable,
        +    LocationSubElementTable
        +} from "../repositories/location.repository.js";
        +import PlotPointRepository, { BookPlotPointsTable } from "../repositories/plotpoint.repository.js";
        +import WorldRepository, {
        +    BookWorldElementsTable,
        +    BookWorldTable
        +} from "../repositories/world.repository.js";
        +import ChapterContentRepository, { BookChapterContentTable } from "../repositories/chaptercontent.repository.js";
        +
        +export default class Upload {
        +    /**
        +     * Prepares a complete book with all related data for synchronization upload.
        +     * Fetches all book-related tables from the database, decrypts encrypted fields
        +     * using the user's encryption key, and returns a complete book object ready for sync.
        +     *
        +     * @param userId - The unique identifier of the user who owns the book
        +     * @param bookId - The unique identifier of the book to upload
        +     * @param lang - The language code for localization ("fr" or "en")
        +     * @returns A promise that resolves to a CompleteBook object containing all decrypted book data
        +     */
        +    static async uploadBookForSync(userId: string, bookId: string, lang: "fr" | "en"): Promise {
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +
        +        const [
        +            encryptedBooks,
        +            encryptedActSummaries,
        +            encryptedAIGuidelines,
        +            encryptedChapters,
        +            encryptedCharacters,
        +            encryptedGuidelines,
        +            encryptedIncidents,
        +            encryptedIssues,
        +            encryptedLocations,
        +            encryptedPlotPoints,
        +            encryptedWorlds
        +        ]: [
        +            EritBooksTable[],
        +            BookActSummariesTable[],
        +            BookAIGuideLineTable[],
        +            BookChaptersTable[],
        +            BookCharactersTable[],
        +            BookGuideLineTable[],
        +            BookIncidentsTable[],
        +            BookIssuesTable[],
        +            BookLocationTable[],
        +            BookPlotPointsTable[],
        +            BookWorldTable[]
        +        ] = await Promise.all([
        +            BookRepo.fetchEritBooksTable(userId, bookId, lang),
        +            ActRepository.fetchBookActSummaries(userId, bookId, lang),
        +            GuidelineRepo.fetchBookAIGuideLine(userId, bookId, lang),
        +            ChapterRepo.fetchBookChapters(userId, bookId, lang),
        +            CharacterRepo.fetchBookCharacters(userId, bookId, lang),
        +            GuidelineRepo.fetchBookGuideLineTable(userId, bookId, lang),
        +            IncidentRepository.fetchBookIncidents(userId, bookId, lang),
        +            IssueRepository.fetchBookIssues(userId, bookId, lang),
        +            LocationRepo.fetchBookLocations(userId, bookId, lang),
        +            PlotPointRepository.fetchBookPlotPoints(userId, bookId, lang),
        +            WorldRepository.fetchBookWorlds(userId, bookId, lang)
        +        ]);
        +
        +        const [
        +            nestedChapterContents,
        +            nestedChapterInfos,
        +            nestedCharacterAttributes,
        +            nestedWorldElements,
        +            nestedLocationElements
        +        ]: [
        +            BookChapterContentTable[][],
        +            BookChapterInfosTable[][],
        +            BookCharactersAttributesTable[][],
        +            BookWorldElementsTable[][],
        +            LocationElementTable[][]
        +        ] = await Promise.all([
        +            Promise.all(encryptedChapters.map((chapter: BookChaptersTable): Promise =>
        +                ChapterContentRepository.fetchBookChapterContents(userId, chapter.chapter_id, lang))),
        +            Promise.all(encryptedChapters.map((chapter: BookChaptersTable): Promise =>
        +                ChapterRepo.fetchBookChapterInfos(userId, chapter.chapter_id, lang))),
        +            Promise.all(encryptedCharacters.map((character: BookCharactersTable): Promise =>
        +                CharacterRepo.fetchBookCharactersAttributes(userId, character.character_id, lang))),
        +            Promise.all(encryptedWorlds.map((world: BookWorldTable): Promise =>
        +                WorldRepository.fetchBookWorldElements(userId, world.world_id, lang))),
        +            Promise.all(encryptedLocations.map((location: BookLocationTable): Promise =>
        +                LocationRepo.fetchLocationElements(userId, location.loc_id, lang)))
        +        ]);
        +
        +        const encryptedChapterContents: BookChapterContentTable[] = nestedChapterContents.flat();
        +        const encryptedChapterInfos: BookChapterInfosTable[] = nestedChapterInfos.flat();
        +        const encryptedCharacterAttributes: BookCharactersAttributesTable[] = nestedCharacterAttributes.flat();
        +        const encryptedWorldElements: BookWorldElementsTable[] = nestedWorldElements.flat();
        +        const encryptedLocationElements: LocationElementTable[] = nestedLocationElements.flat();
        +
        +        const nestedLocationSubElements: LocationSubElementTable[][] = await Promise.all(
        +            encryptedLocationElements.map((element: LocationElementTable): Promise =>
        +                LocationRepo.fetchLocationSubElements(userId, element.element_id, lang))
        +        );
        +        const encryptedLocationSubElements: LocationSubElementTable[] = nestedLocationSubElements.flat();
        +
        +        const eritBooks: EritBooksTable[] = encryptedBooks.map((book: EritBooksTable): EritBooksTable => ({
        +            ...book,
        +            title: System.decryptDataWithUserKey(book.title, userEncryptionKey),
        +            sub_title: book.sub_title ? System.decryptDataWithUserKey(book.sub_title, userEncryptionKey) : null,
        +            summary: book.summary ? System.decryptDataWithUserKey(book.summary, userEncryptionKey) : null,
        +            cover_image: book.cover_image ? System.decryptDataWithUserKey(book.cover_image, userEncryptionKey) : null
        +        }));
        +
        +        const actSummaries: BookActSummariesTable[] = encryptedActSummaries.map((actSummary: BookActSummariesTable): BookActSummariesTable => ({
        +            ...actSummary,
        +            summary: actSummary.summary ? System.decryptDataWithUserKey(actSummary.summary, userEncryptionKey) : null
        +        }));
        +
        +        const aiGuideLine: BookAIGuideLineTable[] = encryptedAIGuidelines.map((guideLine: BookAIGuideLineTable): BookAIGuideLineTable => ({
        +            ...guideLine,
        +            global_resume: guideLine.global_resume ? System.decryptDataWithUserKey(guideLine.global_resume, userEncryptionKey) : null,
        +            themes: guideLine.themes ? System.decryptDataWithUserKey(guideLine.themes, userEncryptionKey) : null,
        +            tone: guideLine.tone ? System.decryptDataWithUserKey(guideLine.tone, userEncryptionKey) : null,
        +            atmosphere: guideLine.atmosphere ? System.decryptDataWithUserKey(guideLine.atmosphere, userEncryptionKey) : null,
        +            current_resume: guideLine.current_resume ? System.decryptDataWithUserKey(guideLine.current_resume, userEncryptionKey) : null
        +        }));
        +
        +        const chapters: BookChaptersTable[] = encryptedChapters.map((chapter: BookChaptersTable): BookChaptersTable => ({
        +            ...chapter,
        +            title: System.decryptDataWithUserKey(chapter.title, userEncryptionKey)
        +        }));
        +
        +        const chapterContents: BookChapterContentTable[] = encryptedChapterContents.map((chapterContent: BookChapterContentTable): BookChapterContentTable => ({
        +            ...chapterContent,
        +            content: chapterContent.content ? JSON.parse(System.decryptDataWithUserKey(chapterContent.content, userEncryptionKey)) : null
        +        }));
        +
        +        const chapterInfos: BookChapterInfosTable[] = encryptedChapterInfos.map((chapterInfo: BookChapterInfosTable): BookChapterInfosTable => ({
        +            ...chapterInfo,
        +            summary: chapterInfo.summary ? System.decryptDataWithUserKey(chapterInfo.summary, userEncryptionKey) : null,
        +            goal: chapterInfo.goal ? System.decryptDataWithUserKey(chapterInfo.goal, userEncryptionKey) : null
        +        }));
        +
        +        const characters: BookCharactersTable[] = encryptedCharacters.map((character: BookCharactersTable): BookCharactersTable => ({
        +            ...character,
        +            first_name: System.decryptDataWithUserKey(character.first_name, userEncryptionKey),
        +            last_name: character.last_name ? System.decryptDataWithUserKey(character.last_name, userEncryptionKey) : null,
        +            category: System.decryptDataWithUserKey(character.category, userEncryptionKey),
        +            title: character.title ? System.decryptDataWithUserKey(character.title, userEncryptionKey) : null,
        +            role: character.role ? System.decryptDataWithUserKey(character.role, userEncryptionKey) : null,
        +            biography: character.biography ? System.decryptDataWithUserKey(character.biography, userEncryptionKey) : null,
        +            history: character.history ? System.decryptDataWithUserKey(character.history, userEncryptionKey) : null
        +        }));
        +
        +        const characterAttributes: BookCharactersAttributesTable[] = encryptedCharacterAttributes.map((attribute: BookCharactersAttributesTable): BookCharactersAttributesTable => ({
        +            ...attribute,
        +            attribute_name: System.decryptDataWithUserKey(attribute.attribute_name, userEncryptionKey),
        +            attribute_value: System.decryptDataWithUserKey(attribute.attribute_value, userEncryptionKey)
        +        }));
        +
        +        const guideLine: BookGuideLineTable[] = encryptedGuidelines.map((guide: BookGuideLineTable): BookGuideLineTable => ({
        +            ...guide,
        +            tone: guide.tone ? System.decryptDataWithUserKey(guide.tone, userEncryptionKey) : null,
        +            atmosphere: guide.atmosphere ? System.decryptDataWithUserKey(guide.atmosphere, userEncryptionKey) : null,
        +            writing_style: guide.writing_style ? System.decryptDataWithUserKey(guide.writing_style, userEncryptionKey) : null,
        +            themes: guide.themes ? System.decryptDataWithUserKey(guide.themes, userEncryptionKey) : null,
        +            symbolism: guide.symbolism ? System.decryptDataWithUserKey(guide.symbolism, userEncryptionKey) : null,
        +            motifs: guide.motifs ? System.decryptDataWithUserKey(guide.motifs, userEncryptionKey) : null,
        +            narrative_voice: guide.narrative_voice ? System.decryptDataWithUserKey(guide.narrative_voice, userEncryptionKey) : null,
        +            pacing: guide.pacing ? System.decryptDataWithUserKey(guide.pacing, userEncryptionKey) : null,
        +            intended_audience: guide.intended_audience ? System.decryptDataWithUserKey(guide.intended_audience, userEncryptionKey) : null,
        +            key_messages: guide.key_messages ? System.decryptDataWithUserKey(guide.key_messages, userEncryptionKey) : null
        +        }));
        +
        +        const incidents: BookIncidentsTable[] = encryptedIncidents.map((incident: BookIncidentsTable): BookIncidentsTable => ({
        +            ...incident,
        +            title: System.decryptDataWithUserKey(incident.title, userEncryptionKey),
        +            summary: incident.summary ? System.decryptDataWithUserKey(incident.summary, userEncryptionKey) : null
        +        }));
        +
        +        const issues: BookIssuesTable[] = encryptedIssues.map((issue: BookIssuesTable): BookIssuesTable => ({
        +            ...issue,
        +            name: System.decryptDataWithUserKey(issue.name, userEncryptionKey)
        +        }));
        +
        +        const locations: BookLocationTable[] = encryptedLocations.map((location: BookLocationTable): BookLocationTable => ({
        +            ...location,
        +            loc_name: System.decryptDataWithUserKey(location.loc_name, userEncryptionKey)
        +        }));
        +
        +        const plotPoints: BookPlotPointsTable[] = encryptedPlotPoints.map((plotPoint: BookPlotPointsTable): BookPlotPointsTable => ({
        +            ...plotPoint,
        +            title: System.decryptDataWithUserKey(plotPoint.title, userEncryptionKey),
        +            summary: plotPoint.summary ? System.decryptDataWithUserKey(plotPoint.summary, userEncryptionKey) : null
        +        }));
        +
        +        const worlds: BookWorldTable[] = encryptedWorlds.map((world: BookWorldTable): BookWorldTable => ({
        +            ...world,
        +            name: System.decryptDataWithUserKey(world.name, userEncryptionKey),
        +            history: world.history ? System.decryptDataWithUserKey(world.history, userEncryptionKey) : null,
        +            politics: world.politics ? System.decryptDataWithUserKey(world.politics, userEncryptionKey) : null,
        +            economy: world.economy ? System.decryptDataWithUserKey(world.economy, userEncryptionKey) : null,
        +            religion: world.religion ? System.decryptDataWithUserKey(world.religion, userEncryptionKey) : null,
        +            languages: world.languages ? System.decryptDataWithUserKey(world.languages, userEncryptionKey) : null
        +        }));
        +
        +        const worldElements: BookWorldElementsTable[] = encryptedWorldElements.map((worldElement: BookWorldElementsTable): BookWorldElementsTable => ({
        +            ...worldElement,
        +            name: System.decryptDataWithUserKey(worldElement.name, userEncryptionKey),
        +            description: worldElement.description ? System.decryptDataWithUserKey(worldElement.description, userEncryptionKey) : null
        +        }));
        +
        +        const locationElements: LocationElementTable[] = encryptedLocationElements.map((locationElement: LocationElementTable): LocationElementTable => ({
        +            ...locationElement,
        +            element_name: System.decryptDataWithUserKey(locationElement.element_name, userEncryptionKey),
        +            element_description: locationElement.element_description ? System.decryptDataWithUserKey(locationElement.element_description, userEncryptionKey) : null
        +        }));
        +
        +        const locationSubElements: LocationSubElementTable[] = encryptedLocationSubElements.map((locationSubElement: LocationSubElementTable): LocationSubElementTable => ({
        +            ...locationSubElement,
        +            sub_elem_name: System.decryptDataWithUserKey(locationSubElement.sub_elem_name, userEncryptionKey),
        +            sub_elem_description: locationSubElement.sub_elem_description ? System.decryptDataWithUserKey(locationSubElement.sub_elem_description, userEncryptionKey) : null
        +        }));
        +
        +        return {
        +            eritBooks,
        +            actSummaries,
        +            aiGuideLine,
        +            chapters,
        +            chapterContents,
        +            chapterInfos,
        +            characters,
        +            characterAttributes,
        +            guideLine,
        +            incidents,
        +            issues,
        +            locations,
        +            plotPoints,
        +            worlds,
        +            worldElements,
        +            locationElements,
        +            locationSubElements
        +        };
        +    }
        +}
        diff --git a/electron/database/models/User.ts b/electron/database/models/User.ts
        index 28c41d6..ecdcc75 100644
        --- a/electron/database/models/User.ts
        +++ b/electron/database/models/User.ts
        @@ -3,24 +3,36 @@ import System from "../System.js";
         import Book, {BookProps} from "./Book.js";
         import {getUserEncryptionKey} from "../keyManager.js";
         
        -interface UserAccount{
        -    firstName:string;
        -    lastName:string;
        -    username:string
        -    authorName:string;
        -    email:string;
        +/**
        + * Represents a user account with basic profile information.
        + */
        +interface UserAccount {
        +    firstName: string;
        +    lastName: string;
        +    username: string;
        +    authorName: string;
        +    email: string;
         }
         
        +/**
        + * Represents the guide tour completion status for various features.
        + */
         export interface GuideTour {
             [key: string]: boolean;
         }
         
        +/**
        + * Summary information for a book associated with a user.
        + */
         interface BookSummary {
             bookId: string;
             title: string;
             subTitle?: string;
         }
         
        +/**
        + * Complete user information response including profile data and associated books.
        + */
         export interface UserInfoResponse {
             id: string;
             name: string;
        @@ -31,13 +43,17 @@ export interface UserInfoResponse {
             authorName: string;
             groupId: number;
             termsAccepted: boolean;
        -    guideTour: any[];
        +    guideTour: GuideTour[];
             books: BookSummary[];
         }
         
        -export default class User{
        +/**
        + * Represents a user entity with encrypted personal information storage.
        + * Handles user data retrieval, creation, and updates with AES-256-CBC encryption.
        + */
        +export default class User {
         
        -    private readonly id:string;
        +    private readonly id: string;
             private firstName: string;
             private lastName: string;
             private username: string;
        @@ -47,7 +63,11 @@ export default class User{
             private groupId: number;
             private termsAccepted: boolean;
         
        -    constructor(id:string){
        +    /**
        +     * Creates a new User instance with the specified identifier.
        +     * @param id - The unique identifier for the user
        +     */
        +    constructor(id: string) {
                 this.id = id;
                 this.firstName = '';
                 this.lastName = '';
        @@ -58,25 +78,35 @@ export default class User{
                 this.groupId = 0;
                 this.termsAccepted = false;
             }
        -    
        +
        +    /**
        +     * Fetches and decrypts the user's information from the database.
        +     * Populates all instance properties with the decrypted values.
        +     * @returns A promise that resolves when user information has been loaded
        +     */
             public async getUserInfos(): Promise {
        -        const data: UserInfosQueryResponse = UserRepo.fetchUserInfos(this.id);
        -        const userKey:string = getUserEncryptionKey(this.id)
        -        this.firstName = System.decryptDataWithUserKey(data.first_name, userKey);
        -        this.lastName = System.decryptDataWithUserKey(data.last_name, userKey);
        -        this.username = System.decryptDataWithUserKey(data.username, userKey);
        -        this.email = System.decryptDataWithUserKey(data.email, userKey);
        -        this.accountVerified = data.account_verified === 1;
        -        this.authorName = data.author_name ? System.decryptDataWithUserKey(data.author_name, userKey) : '';
        -        this.groupId = data.user_group ? data.user_group : 0;
        -        this.termsAccepted = data.term_accepted === 1;
        +        const userInfosData: UserInfosQueryResponse = UserRepo.fetchUserInfos(this.id);
        +        const userEncryptionKey: string = getUserEncryptionKey(this.id);
        +        this.firstName = System.decryptDataWithUserKey(userInfosData.first_name, userEncryptionKey);
        +        this.lastName = System.decryptDataWithUserKey(userInfosData.last_name, userEncryptionKey);
        +        this.username = System.decryptDataWithUserKey(userInfosData.username, userEncryptionKey);
        +        this.email = System.decryptDataWithUserKey(userInfosData.email, userEncryptionKey);
        +        this.accountVerified = userInfosData.account_verified === 1;
        +        this.authorName = userInfosData.author_name ? System.decryptDataWithUserKey(userInfosData.author_name, userEncryptionKey) : '';
        +        this.groupId = userInfosData.user_group ? userInfosData.user_group : 0;
        +        this.termsAccepted = userInfosData.term_accepted === 1;
             }
        -    
        -    public static async returnUserInfos(userId: string):Promise {
        +
        +    /**
        +     * Retrieves complete user information including associated books.
        +     * @param userId - The unique identifier of the user to fetch
        +     * @returns A promise resolving to the complete user information response
        +     */
        +    public static async returnUserInfos(userId: string): Promise {
                 const user: User = new User(userId);
                 await user.getUserInfos();
        -        const books: BookProps[] = await Book.getBooks(userId);
        -        const guideTour: GuideTour[] = [];
        +        const userBooks: BookProps[] = await Book.getBooks(userId);
        +        const guideTourStatus: GuideTour[] = [];
                 return {
                     id: user.getId(),
                     name: user.getFirstName(),
        @@ -87,95 +117,194 @@ export default class User{
                     authorName: user.getAuthorName(),
                     groupId: user.getGroupId(),
                     termsAccepted: user.isTermsAccepted(),
        -            guideTour: guideTour,
        -            books: books.map((book: BookProps):BookSummary => {
        +            guideTour: guideTourStatus,
        +            books: userBooks.map((book: BookProps): BookSummary => {
                         return {
                             bookId: book.id,
                             title: book.title,
                             subTitle: book.subTitle,
                         };
                     })
        -        }
        -    }
        -    
        -    public static async addUser(userId:string,firstName: string, lastName: string, username: string, email: string, notEncryptPassword: string, lang: 'fr' | 'en' = 'fr'): Promise {
        -        const originEmail:string = System.hashElement(email);
        -        const originUsername:string = System.hashElement(username);
        -        const userKey: string = getUserEncryptionKey(userId);
        -        const encryptFirstName: string = System.encryptDataWithUserKey(firstName, userKey);
        -        const encryptLastName: string = System.encryptDataWithUserKey(lastName, userKey);
        -        const encryptUsername: string = System.encryptDataWithUserKey(username, userKey);
        -        const encryptEmail: string = System.encryptDataWithUserKey(email, userKey);
        -        const originalEmail: string = System.hashElement(email);
        -        const originalUsername: string = System.hashElement(username);
        -        return UserRepo.insertUser(userId, encryptFirstName, encryptLastName, encryptUsername, originalUsername, encryptEmail, originalEmail,lang);
        -    }
        -    
        -    public static async updateUserInfos(userKey: string, userId: string, firstName: string, lastName: string, username: string, email: string, authorName?: string, lang: 'fr' | 'en' = 'fr'): Promise {
        -        const encryptFirstName:string = System.encryptDataWithUserKey(firstName,userKey);
        -        const encryptLastName:string = System.encryptDataWithUserKey(lastName,userKey);
        -        const encryptUsername:string = System.encryptDataWithUserKey(username,userKey);
        -        const encryptEmail:string = System.encryptDataWithUserKey(email,userKey);
        -        const originalEmail:string = System.hashElement(email);
        -        const originalUsername:string = System.hashElement(username);
        -        let encryptAuthorName:string = '';
        -        let originalAuthorName: string = '';
        -        if (authorName){
        -            encryptAuthorName = System.encryptDataWithUserKey(authorName,userKey);
        -            originalAuthorName = System.hashElement(authorName);
        -        }
        -        return UserRepo.updateUserInfos(userId, encryptFirstName, encryptLastName, encryptUsername, originalUsername, encryptEmail, originalEmail, originalAuthorName, encryptAuthorName, lang);
        -    }
        -    
        -    public static async getUserAccountInformation(userId: string): Promise {
        -        const data: UserAccountQuery = UserRepo.fetchAccountInformation(userId);
        -        const userKey:string = getUserEncryptionKey(userId)
        -        const userName: string = data.first_name ? System.decryptDataWithUserKey(data.first_name, userKey) : '';
        -        const lastName: string = data.last_name ? System.decryptDataWithUserKey(data.last_name, userKey) : '';
        -        const username: string = data.username ? System.decryptDataWithUserKey(data.username, userKey) : '';
        -        const authorName: string = data.author_name ? System.decryptDataWithUserKey(data.author_name, userKey) : '';
        -        const email: string = data.email ? System.decryptDataWithUserKey(data.email, userKey) : '';
        -        return {
        -            firstName: userName,
        -            lastName: lastName,
        -            username: username,
        -            authorName: authorName,
        -            email: email
                 };
             }
        -    
        +
        +    /**
        +     * Creates a new user in the database with encrypted personal information.
        +     * @param userId - The unique identifier for the new user
        +     * @param firstName - The user's first name (will be encrypted)
        +     * @param lastName - The user's last name (will be encrypted)
        +     * @param username - The user's username (will be encrypted and hashed)
        +     * @param email - The user's email address (will be encrypted and hashed)
        +     * @param notEncryptPassword - The user's password in plain text (unused in current implementation)
        +     * @param lang - The preferred language for the user ('fr' or 'en'), defaults to 'fr'
        +     * @returns A promise resolving to the created user's identifier
        +     */
        +    public static async addUser(
        +        userId: string,
        +        firstName: string,
        +        lastName: string,
        +        username: string,
        +        email: string,
        +        notEncryptPassword: string,
        +        lang: 'fr' | 'en' = 'fr'
        +    ): Promise {
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +        const encryptedFirstName: string = System.encryptDataWithUserKey(firstName, userEncryptionKey);
        +        const encryptedLastName: string = System.encryptDataWithUserKey(lastName, userEncryptionKey);
        +        const encryptedUsername: string = System.encryptDataWithUserKey(username, userEncryptionKey);
        +        const encryptedEmail: string = System.encryptDataWithUserKey(email, userEncryptionKey);
        +        const hashedEmail: string = System.hashElement(email);
        +        const hashedUsername: string = System.hashElement(username);
        +        return UserRepo.insertUser(
        +            userId,
        +            encryptedFirstName,
        +            encryptedLastName,
        +            encryptedUsername,
        +            hashedUsername,
        +            encryptedEmail,
        +            hashedEmail,
        +            lang
        +        );
        +    }
        +
        +    /**
        +     * Updates an existing user's profile information in the database.
        +     * @param userKey - The encryption key for the user's data
        +     * @param userId - The unique identifier of the user to update
        +     * @param firstName - The updated first name (will be encrypted)
        +     * @param lastName - The updated last name (will be encrypted)
        +     * @param username - The updated username (will be encrypted and hashed)
        +     * @param email - The updated email address (will be encrypted and hashed)
        +     * @param authorName - The optional author/pen name (will be encrypted and hashed if provided)
        +     * @param lang - The preferred language for the user ('fr' or 'en'), defaults to 'fr'
        +     * @returns A promise resolving to true if the update was successful
        +     */
        +    public static async updateUserInfos(
        +        userKey: string,
        +        userId: string,
        +        firstName: string,
        +        lastName: string,
        +        username: string,
        +        email: string,
        +        authorName?: string,
        +        lang: 'fr' | 'en' = 'fr'
        +    ): Promise {
        +        const encryptedFirstName: string = System.encryptDataWithUserKey(firstName, userKey);
        +        const encryptedLastName: string = System.encryptDataWithUserKey(lastName, userKey);
        +        const encryptedUsername: string = System.encryptDataWithUserKey(username, userKey);
        +        const encryptedEmail: string = System.encryptDataWithUserKey(email, userKey);
        +        const hashedEmail: string = System.hashElement(email);
        +        const hashedUsername: string = System.hashElement(username);
        +        let encryptedAuthorName: string = '';
        +        let hashedAuthorName: string = '';
        +        if (authorName) {
        +            encryptedAuthorName = System.encryptDataWithUserKey(authorName, userKey);
        +            hashedAuthorName = System.hashElement(authorName);
        +        }
        +        return UserRepo.updateUserInfos(
        +            userId,
        +            encryptedFirstName,
        +            encryptedLastName,
        +            encryptedUsername,
        +            hashedUsername,
        +            encryptedEmail,
        +            hashedEmail,
        +            hashedAuthorName,
        +            encryptedAuthorName,
        +            lang
        +        );
        +    }
        +
        +    /**
        +     * Retrieves and decrypts the user's account information from the database.
        +     * @param userId - The unique identifier of the user
        +     * @returns A promise resolving to the decrypted user account information
        +     */
        +    public static async getUserAccountInformation(userId: string): Promise {
        +        const accountData: UserAccountQuery = UserRepo.fetchAccountInformation(userId);
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +        const decryptedFirstName: string = accountData.first_name ? System.decryptDataWithUserKey(accountData.first_name, userEncryptionKey) : '';
        +        const decryptedLastName: string = accountData.last_name ? System.decryptDataWithUserKey(accountData.last_name, userEncryptionKey) : '';
        +        const decryptedUsername: string = accountData.username ? System.decryptDataWithUserKey(accountData.username, userEncryptionKey) : '';
        +        const decryptedAuthorName: string = accountData.author_name ? System.decryptDataWithUserKey(accountData.author_name, userEncryptionKey) : '';
        +        const decryptedEmail: string = accountData.email ? System.decryptDataWithUserKey(accountData.email, userEncryptionKey) : '';
        +        return {
        +            firstName: decryptedFirstName,
        +            lastName: decryptedLastName,
        +            username: decryptedUsername,
        +            authorName: decryptedAuthorName,
        +            email: decryptedEmail
        +        };
        +    }
        +
        +    /**
        +     * Gets the unique identifier of the user.
        +     * @returns The user's unique identifier
        +     */
             public getId(): string {
                 return this.id;
             }
        -    
        +
        +    /**
        +     * Gets the user's first name.
        +     * @returns The user's first name
        +     */
             public getFirstName(): string {
                 return this.firstName;
             }
        -    
        +
        +    /**
        +     * Gets the user's last name.
        +     * @returns The user's last name
        +     */
             public getLastName(): string {
                 return this.lastName;
             }
        -    
        +
        +    /**
        +     * Gets the user's username.
        +     * @returns The user's username
        +     */
             public getUsername(): string {
                 return this.username;
             }
        -    
        +
        +    /**
        +     * Gets the user's email address.
        +     * @returns The user's email address
        +     */
             public getEmail(): string {
                 return this.email;
             }
        -    
        +
        +    /**
        +     * Checks if the user's account has been verified.
        +     * @returns True if the account is verified, false otherwise
        +     */
             public isAccountVerified(): boolean {
                 return this.accountVerified;
             }
        -    
        +
        +    /**
        +     * Checks if the user has accepted the terms of service.
        +     * @returns True if the terms have been accepted, false otherwise
        +     */
             public isTermsAccepted(): boolean {
                 return this.termsAccepted;
             }
        -    
        +
        +    /**
        +     * Gets the user's group identifier.
        +     * @returns The user's group identifier
        +     */
             public getGroupId(): number {
                 return this.groupId;
             }
        -    
        +
        +    /**
        +     * Gets the user's author/pen name.
        +     * @returns The user's author name
        +     */
             public getAuthorName(): string {
                 return this.authorName;
             }
        diff --git a/electron/database/models/World.ts b/electron/database/models/World.ts
        new file mode 100644
        index 0000000..96caf4b
        --- /dev/null
        +++ b/electron/database/models/World.ts
        @@ -0,0 +1,268 @@
        +import { getUserEncryptionKey } from "../keyManager.js";
        +import System from "../System.js";
        +import WorldRepository, { WorldElementValue, WorldQuery } from "../repositories/world.repository.js";
        +
        +export interface SyncedWorld {
        +    id: string;
        +    name: string;
        +    lastUpdate: number;
        +    elements: SyncedWorldElement[];
        +}
        +
        +export interface SyncedWorldElement {
        +    id: string;
        +    name: string;
        +    lastUpdate: number;
        +}
        +
        +export interface WorldElement {
        +    id: string;
        +    name: string;
        +    description: string;
        +    type?: number;
        +}
        +
        +export interface WorldProps {
        +    id: string;
        +    name: string;
        +    history: string;
        +    politics: string;
        +    economy: string;
        +    religion: string;
        +    languages: string;
        +    laws: WorldElement[];
        +    biomes: WorldElement[];
        +    issues: WorldElement[];
        +    customs: WorldElement[];
        +    kingdoms: WorldElement[];
        +    climate: WorldElement[];
        +    resources: WorldElement[];
        +    wildlife: WorldElement[];
        +    arts: WorldElement[];
        +    ethnicGroups: WorldElement[];
        +    socialClasses: WorldElement[];
        +    importantCharacters: WorldElement[];
        +}
        +
        +/**
        + * Mapping of element type keys to their corresponding numeric type identifiers.
        + */
        +const ELEMENT_TYPE_MAP: Record = {
        +    laws: 1,
        +    biomes: 2,
        +    issues: 3,
        +    customs: 4,
        +    kingdoms: 5,
        +    climate: 6,
        +    resources: 7,
        +    wildlife: 8,
        +    arts: 9,
        +    ethnicGroups: 10,
        +    socialClasses: 11,
        +    importantCharacters: 12
        +};
        +
        +/**
        + * Mapping of numeric type identifiers to their corresponding WorldProps keys.
        + */
        +const ELEMENT_TYPE_KEYS: Record = {
        +    1: 'laws',
        +    2: 'biomes',
        +    3: 'issues',
        +    4: 'customs',
        +    5: 'kingdoms',
        +    6: 'climate',
        +    7: 'resources',
        +    8: 'wildlife',
        +    9: 'arts',
        +    10: 'ethnicGroups',
        +    11: 'socialClasses',
        +    12: 'importantCharacters'
        +};
        +
        +export default class World {
        +    /**
        +     * Creates a new world for a book.
        +     * @param userId - The unique identifier of the user creating the world
        +     * @param bookId - The unique identifier of the book to associate the world with
        +     * @param worldName - The name of the new world
        +     * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
        +     * @param existingWorldId - Optional existing world ID for syncing purposes
        +     * @returns The unique identifier of the newly created world
        +     * @throws Error if a world with the same name already exists for this book
        +     */
        +    public static addNewWorld(userId: string, bookId: string, worldName: string, lang: 'fr' | 'en' = 'fr', existingWorldId?: string): string {
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +        const hashedWorldName: string = System.hashElement(worldName);
        +        if (!existingWorldId && WorldRepository.checkWorldExist(userId, bookId, hashedWorldName, lang)) {
        +            throw new Error(lang === "fr" ? `Tu as déjà un monde ${worldName}.` : `You already have a world named ${worldName}.`);
        +        }
        +        const encryptedWorldName: string = System.encryptDataWithUserKey(worldName, userEncryptionKey);
        +        const worldId: string = existingWorldId || System.createUniqueId();
        +        return WorldRepository.insertNewWorld(worldId, userId, bookId, encryptedWorldName, hashedWorldName, lang);
        +    }
        +
        +    /**
        +     * Retrieves all worlds and their elements for a specific book.
        +     * @param userId - The unique identifier of the user
        +     * @param bookId - The unique identifier of the book
        +     * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
        +     * @returns An array of WorldProps objects containing all world data and their elements
        +     */
        +    public static getWorlds(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): WorldProps[] {
        +        const worldQueryResults: WorldQuery[] = WorldRepository.fetchWorlds(userId, bookId, lang);
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +        const worlds: WorldProps[] = [];
        +
        +        for (const queryRow of worldQueryResults) {
        +            const existingWorld: WorldProps | undefined = worlds.find((world: WorldProps) => world.id === queryRow.world_id);
        +
        +            if (!existingWorld) {
        +                const newWorld: WorldProps = {
        +                    id: queryRow.world_id,
        +                    name: System.decryptDataWithUserKey(queryRow.world_name, userEncryptionKey),
        +                    history: queryRow.history ? System.decryptDataWithUserKey(queryRow.history, userEncryptionKey) : '',
        +                    politics: queryRow.politics ? System.decryptDataWithUserKey(queryRow.politics, userEncryptionKey) : '',
        +                    economy: queryRow.economy ? System.decryptDataWithUserKey(queryRow.economy, userEncryptionKey) : '',
        +                    religion: queryRow.religion ? System.decryptDataWithUserKey(queryRow.religion, userEncryptionKey) : '',
        +                    languages: queryRow.languages ? System.decryptDataWithUserKey(queryRow.languages, userEncryptionKey) : '',
        +                    laws: [],
        +                    biomes: [],
        +                    issues: [],
        +                    customs: [],
        +                    kingdoms: [],
        +                    climate: [],
        +                    resources: [],
        +                    wildlife: [],
        +                    arts: [],
        +                    ethnicGroups: [],
        +                    socialClasses: [],
        +                    importantCharacters: [],
        +                };
        +
        +                worlds.push(newWorld);
        +
        +                if (queryRow.element_type) {
        +                    const worldElement: WorldElement = {
        +                        id: queryRow.element_id as string,
        +                        name: queryRow.element_name ? System.decryptDataWithUserKey(queryRow.element_name, userEncryptionKey) : '',
        +                        description: queryRow.element_description ? System.decryptDataWithUserKey(queryRow.element_description, userEncryptionKey) : ''
        +                    };
        +
        +                    const elementKey: keyof WorldProps | undefined = ELEMENT_TYPE_KEYS[queryRow.element_type];
        +                    if (elementKey) {
        +                        (worlds[worlds.length - 1][elementKey] as WorldElement[]).push(worldElement);
        +                    }
        +                }
        +            } else {
        +                const worldElement: WorldElement = {
        +                    id: queryRow.element_id as string,
        +                    name: queryRow.element_name ? System.decryptDataWithUserKey(queryRow.element_name, userEncryptionKey) : '',
        +                    description: queryRow.element_description ? System.decryptDataWithUserKey(queryRow.element_description, userEncryptionKey) : ''
        +                };
        +
        +                const elementKey: keyof WorldProps | undefined = ELEMENT_TYPE_KEYS[queryRow.element_type as number];
        +                if (elementKey) {
        +                    (existingWorld[elementKey] as WorldElement[]).push(worldElement);
        +                }
        +            }
        +        }
        +        return worlds;
        +    }
        +
        +    /**
        +     * Updates a world's properties and all its elements.
        +     * @param userId - The unique identifier of the user
        +     * @param world - The WorldProps object containing updated world data
        +     * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
        +     * @returns True if the update was successful, false otherwise
        +     */
        +    public static updateWorld(userId: string, world: WorldProps, lang: 'fr' | 'en' = 'fr'): boolean {
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +        const encryptedName: string = world.name ? System.encryptDataWithUserKey(world.name, userEncryptionKey) : '';
        +        const encryptedHistory: string = world.history ? System.encryptDataWithUserKey(world.history, userEncryptionKey) : '';
        +        const encryptedPolitics: string = world.politics ? System.encryptDataWithUserKey(world.politics, userEncryptionKey) : '';
        +        const encryptedEconomy: string = world.economy ? System.encryptDataWithUserKey(world.economy, userEncryptionKey) : '';
        +        const encryptedReligion: string = world.religion ? System.encryptDataWithUserKey(world.religion, userEncryptionKey) : '';
        +        const encryptedLanguages: string = world.languages ? System.encryptDataWithUserKey(world.languages, userEncryptionKey) : '';
        +
        +        let elementsToUpdate: WorldElementValue[] = [];
        +        const elementCategories: { key: keyof WorldProps; elements: WorldElement[] }[] = [
        +            { key: 'laws', elements: world.laws },
        +            { key: 'biomes', elements: world.biomes },
        +            { key: 'issues', elements: world.issues },
        +            { key: 'customs', elements: world.customs },
        +            { key: 'kingdoms', elements: world.kingdoms },
        +            { key: 'climate', elements: world.climate },
        +            { key: 'resources', elements: world.resources },
        +            { key: 'wildlife', elements: world.wildlife },
        +            { key: 'arts', elements: world.arts },
        +            { key: 'ethnicGroups', elements: world.ethnicGroups },
        +            { key: 'socialClasses', elements: world.socialClasses },
        +            { key: 'importantCharacters', elements: world.importantCharacters }
        +        ];
        +
        +        elementCategories.forEach(({ key, elements: categoryElements }) => {
        +            elementsToUpdate = elementsToUpdate.concat(categoryElements.map((worldElement: WorldElement) => {
        +                const encryptedElementName: string = System.encryptDataWithUserKey(worldElement.name, userEncryptionKey);
        +                const hashedElementName: string = System.hashElement(worldElement.name);
        +                const encryptedDescription: string = worldElement.description ? System.encryptDataWithUserKey(worldElement.description, userEncryptionKey) : '';
        +                const elementTypeId: number = World.getElementTypes(key);
        +
        +                return {
        +                    id: worldElement.id,
        +                    name: encryptedElementName,
        +                    hashedName: hashedElementName,
        +                    description: encryptedDescription,
        +                    type: elementTypeId
        +                };
        +            }));
        +        });
        +
        +        WorldRepository.updateWorld(userId, world.id, encryptedName, System.hashElement(world.name), encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, System.timeStampInSeconds(), lang);
        +        return WorldRepository.updateWorldElements(userId, elementsToUpdate, lang);
        +    }
        +
        +    /**
        +     * Adds a new element to an existing world.
        +     * @param userId - The unique identifier of the user
        +     * @param worldId - The unique identifier of the world to add the element to
        +     * @param elementName - The name of the new element
        +     * @param elementType - The type of element (e.g., 'laws', 'biomes', 'customs')
        +     * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
        +     * @param existingElementId - Optional existing element ID for syncing purposes
        +     * @returns The unique identifier of the newly created element
        +     * @throws Error if an element with the same name already exists in this world
        +     */
        +    public static addNewElementToWorld(userId: string, worldId: string, elementName: string, elementType: string, lang: 'fr' | 'en' = 'fr', existingElementId?: string): string {
        +        const userEncryptionKey: string = getUserEncryptionKey(userId);
        +        const hashedElementName: string = System.hashElement(elementName);
        +        if (!existingElementId && WorldRepository.checkElementExist(worldId, hashedElementName, lang)) {
        +            throw new Error(lang === "fr" ? `Vous avez déjà un élément avec ce nom ${elementName}.` : `You already have an element named ${elementName}.`);
        +        }
        +        const elementTypeId: number = World.getElementTypes(elementType);
        +        const encryptedElementName: string = System.encryptDataWithUserKey(elementName, userEncryptionKey);
        +        const elementId: string = existingElementId || System.createUniqueId();
        +        return WorldRepository.insertNewElement(userId, elementId, elementTypeId, worldId, encryptedElementName, hashedElementName, lang);
        +    }
        +
        +    /**
        +     * Converts an element type string key to its corresponding numeric identifier.
        +     * @param elementType - The element type key (e.g., 'laws', 'biomes', 'customs')
        +     * @returns The numeric identifier for the element type, or 0 if not found
        +     */
        +    public static getElementTypes(elementType: string): number {
        +        return ELEMENT_TYPE_MAP[elementType] ?? 0;
        +    }
        +
        +    /**
        +     * Removes an element from a world.
        +     * @param userId - The unique identifier of the user
        +     * @param elementId - The unique identifier of the element to remove
        +     * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr'
        +     * @returns True if the deletion was successful, false otherwise
        +     */
        +    public static removeElementFromWorld(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
        +        return WorldRepository.deleteElement(userId, elementId, lang);
        +    }
        +}
        diff --git a/electron/database/repositories/incident.repository.ts b/electron/database/repositories/incident.repository.ts
        index 7a93baa..69d7fe4 100644
        --- a/electron/database/repositories/incident.repository.ts
        +++ b/electron/database/repositories/incident.repository.ts
        @@ -33,7 +33,7 @@ export default class IncidentRepository {
              * @returns An array of incidents with their ID, title, and summary
              * @throws Error if the database query fails
              */
        -    public static fetchAllIncidents(userId: string, bookId: string, lang: 'fr' | 'en'): IncidentQuery[] {
        +    public static fetchAllIncitentIncidents(userId: string, bookId: string, lang: 'fr' | 'en'): IncidentQuery[] {
                 try {
                     const db: Database = System.getDb();
                     const query: string = 'SELECT incident_id, title, summary FROM book_incidents WHERE author_id=? AND book_id=?';