Files
ERitors-Scribe-Desktop/electron/database/models/Book.ts
natreex 209dc6f85a Remove CharacterComponent and CharacterDetail components
- Deleted `CharacterComponent` and `CharacterDetail` files from the project.
- Refactored related logic to improve code maintainability and reduce redundancy.
2026-02-05 14:12:08 -05:00

731 lines
25 KiB
TypeScript

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<BookProps[]> {
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<BookProps> => {
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<string> {
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<BookProps> {
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 = '';
}
}
}