import System from '../System.js'; import { getUserEncryptionKey } from '../keyManager.js'; import BookRepo, { BookQuery, BookToolsTable, 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 { BookSpellsTable } from "../repositories/spell.repo.js"; import { BookSpellTagsTable } from "../repositories/spelltag.repo.js"; import { SyncedSpell, SyncedSpellTag } from "./Spell.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 RemovedItem from "./RemovedItem.js"; export interface SyncedBookTools { lastUpdate: number; charactersEnabled: boolean; worldsEnabled: boolean; locationsEnabled: boolean; spellsEnabled: boolean; } export interface BookToolsSettings { characters: boolean; worlds: boolean; locations: boolean; spells: boolean; } export interface BookProps { bookId: string; type: string; authorId: string; title: string; subTitle?: string; summary?: string; serieId?: number | null; seriesId?: string | null; desiredReleaseDate?: string | null; desiredWordCount?: number | null; wordCount?: number; coverImage?: string | null; bookMeta?: string; tools?: BookToolsSettings; } export interface CompleteBook { eritBooks: EritBooksTable[]; actSummaries: BookActSummariesTable[]; aiGuideLine: BookAIGuideLineTable[]; chapters: BookChaptersTable[]; chapterContents: BookChapterContentTable[]; chapterInfos: BookChapterInfosTable[]; characters: BookCharactersTable[]; characterAttributes: BookCharactersAttributesTable[]; guideLine: BookGuideLineTable[]; incidents: BookIncidentsTable[]; issues: BookIssuesTable[]; locations: BookLocationTable[]; plotPoints: BookPlotPointsTable[]; worlds: BookWorldTable[]; worldElements: BookWorldElementsTable[]; locationElements: LocationElementTable[]; locationSubElements: LocationSubElementTable[]; bookTools: BookToolsTable[]; spells: BookSpellsTable[]; spellTags: BookSpellTagsTable[]; } export interface SyncedBook { id: string; type: string; title: string; subTitle: string | null; lastUpdate: number; chapters: SyncedChapter[]; characters: SyncedCharacter[]; locations: SyncedLocation[]; worlds: SyncedWorld[]; incidents: SyncedIncident[]; plotPoints: SyncedPlotPoint[]; issues: SyncedIssue[]; actSummaries: SyncedActSummary[]; guideLine: SyncedGuideLine | null; aiGuideLine: SyncedAIGuideLine | null; bookTools: SyncedBookTools | null; spells: SyncedSpell[]; spellTags: SyncedSpellTag[]; } export interface BookSyncCompare { id: string; chapters: string[]; chapterContents: string[]; chapterInfos: string[]; characters: string[]; characterAttributes: string[]; locations: string[]; locationElements: string[]; locationSubElements: string[]; worlds: string[]; worldElements: string[]; incidents: string[]; plotPoints: string[]; issues: string[]; actSummaries: string[]; guideLine: boolean; aiGuideLine: boolean; bookTools: boolean; spells: string[]; spellTags: string[]; } export interface CompleteBookData { bookId: string; title: string; subTitle: string; summary: string; coverImage: string; userInfos: { firstName: string; lastName: string; authorName: string; }, chapters: CompleteChapterContent[]; } // ===== SERIES TABLE INTERFACES (for sync) ===== export interface SeriesTable { series_id: string; user_id: string; name: string; hashed_name: string; description: string | null; cover_image: string | null; last_update: number; } export interface SeriesBooksTable { series_id: string; book_id: string; book_order: number; last_update: number; } export interface SeriesCharactersTable { character_id: string; series_id: string; user_id: string; first_name: string; last_name: string | null; nickname: string | null; age: number | null; gender: string | null; species: string | null; nationality: string | null; status: string | null; title: string | null; category: string; image: string | null; role: string | null; biography: string | null; history: string | null; speech_pattern: string | null; catchphrase: string | null; residence: string | null; notes: string | null; color: string | null; last_update: number; } export interface SeriesCharacterAttributesTable { attr_id: string; character_id: string; user_id: string; attribute_name: string; attribute_value: string; last_update: number; } export interface SeriesWorldsTable { world_id: string; series_id: string; user_id: string; name: string; hashed_name: string; history: string | null; politics: string | null; economy: string | null; religion: string | null; languages: string | null; last_update: number; } export interface SeriesWorldElementsTable { element_id: string; world_id: string; user_id: string; element_type: number; name: string; original_name: string; description: string | null; last_update: number; } export interface SeriesLocationsTable { loc_id: string; series_id: string; user_id: string; loc_name: string; loc_original_name: string; last_update: number; } export interface SeriesLocationElementsTable { element_id: string; location_id: string; user_id: string; element_name: string; original_name: string; element_description: string | null; last_update: number; } export interface SeriesLocationSubElementsTable { sub_element_id: string; element_id: string; user_id: string; sub_elem_name: string; original_name: string; sub_elem_description: string | null; last_update: number; } export interface SeriesSpellsTable { spell_id: string; series_id: string; user_id: string; name: string; name_hash: string; description: string; appearance: string; tags: string; power_level: string | null; components: string | null; limitations: string | null; notes: string | null; last_update: number; } export interface SeriesSpellTagsTable { tag_id: string; series_id: string; user_id: string; name: string; hashed_name: string; color: string | null; last_update: number; } // ===== COMPLETE SERIES INTERFACE (for full sync) ===== export interface CompleteSeries { series: SeriesTable[]; seriesBooks: SeriesBooksTable[]; seriesCharacters: SeriesCharactersTable[]; seriesCharacterAttributes: SeriesCharacterAttributesTable[]; seriesWorlds: SeriesWorldsTable[]; seriesWorldElements: SeriesWorldElementsTable[]; seriesLocations: SeriesLocationsTable[]; seriesLocationElements: SeriesLocationElementsTable[]; seriesLocationSubElements: SeriesLocationSubElementsTable[]; seriesSpells: SeriesSpellsTable[]; seriesSpellTags: SeriesSpellTagsTable[]; } // ===== SYNCED SERIES INTERFACES (lightweight, for comparison) ===== export interface SyncedSeriesBook { bookId: string; order: number; lastUpdate: number; } export interface SyncedSeriesCharacterAttribute { id: string; name: string; lastUpdate: number; } export interface SyncedSeriesCharacter { id: string; name: string; lastUpdate: number; attributes: SyncedSeriesCharacterAttribute[]; } export interface SyncedSeriesWorldElement { id: string; name: string; lastUpdate: number; } export interface SyncedSeriesWorld { id: string; name: string; lastUpdate: number; elements: SyncedSeriesWorldElement[]; } export interface SyncedSeriesLocationSubElement { id: string; name: string; lastUpdate: number; } export interface SyncedSeriesLocationElement { id: string; name: string; lastUpdate: number; subElements: SyncedSeriesLocationSubElement[]; } export interface SyncedSeriesLocation { id: string; name: string; lastUpdate: number; elements: SyncedSeriesLocationElement[]; } export interface SyncedSeriesSpell { id: string; name: string; lastUpdate: number; } export interface SyncedSeriesSpellTag { id: string; name: string; lastUpdate: number; } export interface SyncedSeries { id: string; name: string; description: string | null; lastUpdate: number; books: SyncedSeriesBook[]; characters: SyncedSeriesCharacter[]; worlds: SyncedSeriesWorld[]; locations: SyncedSeriesLocation[]; spells: SyncedSeriesSpell[]; spellTags: SyncedSeriesSpellTag[]; } export default class Book { private readonly id: string; private type: string; private authorId: string; private title: string; private subTitle: string; private summary: string; private serieId: number; private desiredReleaseDate: string; private desiredWordCount: number; private wordCount: number; private cover: 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) { this.authorId = authorId; } else { this.authorId = ''; } this.title = ''; this.subTitle = ''; this.summary = ''; this.serieId = 0; this.desiredReleaseDate = ''; this.desiredWordCount = 0; this.wordCount = 0; 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) { throw new Error( lang === 'fr' ? "Clé d'encryption utilisateur non trouvée." : 'User encryption key not found.' ); } const books: BookQuery[] = BookRepo.fetchBooks(userId, lang); if (!books || books.length === 0) { return []; } return await Promise.all( books.map(async (book: BookQuery): Promise => { return { bookId: book.book_id, type: book.type, authorId: book.author_id, title: System.decryptDataWithUserKey(book.title, userKey), subTitle: book.sub_title ? System.decryptDataWithUserKey(book.sub_title, userKey) : '', summary: book.summary ? System.decryptDataWithUserKey(book.summary, userKey) : '', serieId: book.serie_id || 0, desiredReleaseDate: book.desired_release_date || '', desiredWordCount: book.desired_word_count || 0, wordCount: book.words_count || 0, coverImage: book.cover_image ? Cover.getPicture(userId, userKey, book.cover_image) : '', }; }) ?? [] ); } /** * 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) { newBookId = bookId; } else { newBookId = System.createUniqueId(); } return BookRepo.insertBook(newBookId, userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, type, serie, publicationDate, desiredWordCount, lang); } /** * 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); const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang); // Récupérer le seriesId depuis series_books const seriesId: string | null = BookRepo.fetchBookSeriesId(bookId, lang); return { bookId: book.getId(), type: book.getType(), authorId: book.getAuthorId(), title: book.getTitle(), subTitle: book.getSubTitle(), summary: book.getSummary(), serieId: book.getSerieId(), seriesId: seriesId, desiredReleaseDate: book.getDesiredReleaseDate(), desiredWordCount: book.getDesiredWordCount(), wordCount: book.getWordCount(), coverImage: book.getCover(), tools: { characters: bookTools ? bookTools.characters_enabled === 1 : false, worlds: bookTools ? bookTools.worlds_enabled === 1 : false, locations: bookTools ? bookTools.locations_enabled === 1 : false, spells: bookTools ? bookTools.spells_enabled === 1 : false } }; } /** * 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) : ''; return BookRepo.updateBookBasicInformation(userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, publicationDate, wordCount, bookId, 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 deletedAt - The timestamp of deletion (from UI via System.timeStampInSeconds()) * @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, deletedAt: number, lang: 'fr' | 'en' = 'fr'): boolean { const deleted: boolean = BookRepo.deleteBook(userId, bookId, lang); if (deleted) { RemovedItem.deleteTracker(userId, bookId, 'erit_books', bookId, deletedAt, lang); } return deleted; } public static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters' | 'worlds' | 'locations' | 'spells', enabled: boolean, lang: 'fr' | 'en' = 'fr'): boolean { const columnName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' | 'spells_enabled' = `${toolName}_enabled` as 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' | 'spells_enabled'; return BookRepo.updateBookToolSetting(userId, bookId, columnName, enabled, System.timeStampInSeconds(), 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 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 = bookData.title ? System.decryptDataWithUserKey(bookData.title, userKey) : ''; const decryptedChapters: CompleteChapterContent[] = []; for (const chapter of chapters) { decryptedChapters.push({ id: '', title: chapter.title ? System.decryptDataWithUserKey(chapter.title, userKey) : '', content: chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '', order: chapter.chapter_order }) } const coverImage: string = bookData.cover_image ? Cover.getPicture(userId, userKey, bookData.cover_image, lang) : ''; return { bookId: id, title: bookTitle, 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) : '', lastName: userInfos.last_name ? System.decryptDataWithUserKey(userInfos.last_name, userKey) : '', authorName: userInfos.author_name ? System.decryptDataWithUserKey(userInfos.author_name, userKey) : '', }, chapters: decryptedChapters }; } /** * 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 bookData: BookQuery = BookRepo.fetchBook(this.id, userId, lang); const userKey: string = getUserEncryptionKey(userId); 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 = ''; this.subTitle = ''; this.summary = ''; this.serieId = 0; this.desiredReleaseDate = ''; this.wordCount = 0; this.cover = ''; } } }