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.
This commit is contained in:
natreex
2026-01-12 13:38:10 -05:00
parent d9bf089e32
commit cf6fb97bf0
19 changed files with 3643 additions and 2509 deletions

View File

@@ -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<ActProps[]> {
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<boolean> {
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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +1,18 @@
import System from "../System.js";
import { getUserEncryptionKey } from "../keyManager.js";
import Book, { CompleteBookData } from "./Book.js";
import ChapterRepo, { import ChapterRepo, {
ActChapterQuery, ActChapterQuery,
ChapterQueryResult, ChapterQueryResult,
ChapterContentQueryResult,
LastChapterResult,
CompanionContentQueryResult,
ChapterStoryQueryResult, ChapterStoryQueryResult,
ContentQueryResult LastChapterResult
} from "../repositories/chapter.repository.js"; } from "../repositories/chapter.repository.js";
import System from "../System.js"; import { ActChapter, ActStory } from "./Act.js";
import {getUserEncryptionKey} from "../keyManager.js"; import ChapterContentRepository, {
ChapterContentQueryResult,
CompanionContentQueryResult,
ContentQueryResult
} from "../repositories/chaptercontent.repository.js";
export interface ChapterContent { export interface ChapterContent {
version: number; version: number;
@@ -28,287 +32,427 @@ export interface ChapterProps {
chapterContent?: ChapterContent 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 { export interface CompanionContent {
version: number; version: number;
content: string; content: string;
wordsCount: number; wordsCount: number;
} }
export interface ActStory { export interface SyncedChapter {
actId: number; id: string;
summary: string; name: string;
chapterSummary: string; lastUpdate: number;
chapterGoal: string; contents: SyncedChapterContent[];
incidents: IncidentStory[]; info: SyncedChapterInfo | null;
plotPoints: PlotPointStory[];
} }
export interface IncidentStory { export interface SyncedChapterContent {
incidentTitle: string; id: string;
incidentSummary: string; lastUpdate: number;
chapterSummary: string;
chapterGoal: string;
} }
export interface PlotPointStory { export interface SyncedChapterInfo {
plotTitle: string; id: string;
plotSummary: string; lastUpdate: number;
chapterSummary: string; }
chapterGoal: string;
export interface CompleteChapterContent {
id: string;
title: string;
content: string;
order: number;
version?: number;
}
interface TipTapNode {
type?: string;
text?: string;
content?: TipTapNode[];
attrs?: Record<string, unknown>;
marks?: TipTapMark[];
}
interface TipTapMark {
type: string;
attrs?: Record<string, unknown>;
} }
export default class Chapter { 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[] { public static getAllChaptersFromABook(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps[] {
const chapters: ChapterQueryResult[] = ChapterRepo.fetchAllChapterFromABook(userId, bookId, lang); const chapterQueryResults: ChapterQueryResult[] = ChapterRepo.fetchAllChapterFromABook(userId, bookId, lang);
let returnChapters: ChapterProps[] = []; const decryptedChapters: ChapterProps[] = [];
const userKey: string = getUserEncryptionKey(userId); const userEncryptionKey: string = getUserEncryptionKey(userId);
for (const chapter of chapters) {
const title: string = System.decryptDataWithUserKey(chapter.title, userKey); for (const chapterResult of chapterQueryResults) {
returnChapters.push({ const decryptedTitle: string = System.decryptDataWithUserKey(chapterResult.title, userEncryptionKey);
chapterId: chapter.chapter_id, decryptedChapters.push({
title: title, chapterId: chapterResult.chapter_id,
chapterOrder: chapter.chapter_order 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[] { public static getAllChapterFromActs(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ActChapter[] {
const query: ActChapterQuery[] = ChapterRepo.fetchAllChapterForActs(userId, bookId, lang); const actChapterQueryResults: ActChapterQuery[] = ChapterRepo.fetchAllChapterForActs(userId, bookId, lang);
let chapters: ActChapter[] = []; const actChapters: ActChapter[] = [];
let tempChapter: { id: string, title: string }[] = [] const decryptedTitleCache: { id: string; title: string }[] = [];
const userKey: string = getUserEncryptionKey(userId); const userEncryptionKey: string = getUserEncryptionKey(userId);
if (query.length > 0) {
for (const chapter of query) { if (actChapterQueryResults.length === 0) {
let decryptTitle: string = ''; return [];
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 []
} }
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 { 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 chapterContentResult: ChapterContentQueryResult = ChapterContentRepository.fetchWholeChapter(userId, chapterId, version, lang);
const userKey: string = getUserEncryptionKey(userId); const userEncryptionKey: string = getUserEncryptionKey(userId);
if (bookId) { if (bookId) {
ChapterRepo.updateLastChapterRecord(userId, bookId, chapterId, version, lang); ChapterRepo.updateLastChapterRecord(userId, bookId, chapterId, version, lang);
} }
return { return {
chapterId: chapter.chapter_id, chapterId: chapterContentResult.chapter_id,
title: System.decryptDataWithUserKey(chapter.title, userKey), title: System.decryptDataWithUserKey(chapterContentResult.title, userEncryptionKey),
chapterOrder: chapter.chapter_order, chapterOrder: chapterContentResult.chapter_order,
chapterContent: { chapterContent: {
content: chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '', content: chapterContentResult.content ? System.decryptDataWithUserKey(chapterContentResult.content, userEncryptionKey) : '',
version: version, 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 { 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 userEncryptionKey: string = getUserEncryptionKey(userId);
const encryptContent: string = System.encryptDataWithUserKey(JSON.stringify(content), userKey); const encryptedContent: string = System.encryptDataWithUserKey(JSON.stringify(content), userEncryptionKey);
/*if (version === 2){ return ChapterContentRepository.updateChapterContent(userId, chapterId, version, encryptedContent, wordsCount, System.timeStampInSeconds(), lang);
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);
} }
/**
* 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 { public static getLastChapter(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps | null {
const lastChapter: LastChapterResult | null = ChapterRepo.fetchLastChapter(userId, bookId, lang); const lastChapterRecord: LastChapterResult | null = ChapterRepo.fetchLastChapter(userId, bookId, lang);
if (lastChapter) {
return Chapter.getWholeChapter(userId, lastChapter.chapter_id, lastChapter.version, 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) { const chapterContentResults: ChapterContentQueryResult[] = ChapterContentRepository.fetchLastChapterContent(userId, bookId, lang);
return null
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 { return {
chapterId: chapterData.chapter_id, chapterId: firstChapterContent.chapter_id,
title: chapterData.title ? System.decryptDataWithUserKey(chapterData.title, userKey) : '', title: firstChapterContent.title ? System.decryptDataWithUserKey(firstChapterContent.title, userEncryptionKey) : '',
chapterOrder: chapterData.chapter_order, chapterOrder: firstChapterContent.chapter_order,
chapterContent: { chapterContent: {
content: chapterData.content ? System.decryptDataWithUserKey(chapterData.content, userKey) : '', content: firstChapterContent.content ? System.decryptDataWithUserKey(firstChapterContent.content, userEncryptionKey) : '',
version: chapterData.version, version: firstChapterContent.version,
wordsCount: chapterData.words_count 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 { 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 hashedTitle: string = System.hashElement(title);
const userKey: string = getUserEncryptionKey(userId); const userEncryptionKey: string = getUserEncryptionKey(userId);
const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey); const encryptedTitle: string = System.encryptDataWithUserKey(title, userEncryptionKey);
if (!existingChapterId && ChapterRepo.checkNameDuplication(userId, bookId, hashedTitle, lang)) { 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.`); throw new Error(lang === 'fr' ? `Ce nom de chapitre existe déjà.` : `This chapter name already exists.`);
} }
const chapterId: string = existingChapterId || System.createUniqueId(); const chapterId: string = existingChapterId || System.createUniqueId();
return ChapterRepo.insertChapter(chapterId, userId, bookId, encryptedTitle, hashedTitle, wordsCount, chapterOrder, lang); 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 { public static removeChapter(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return ChapterRepo.deleteChapter(userId, chapterId, lang); 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 { 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(); const chapterInfoId: string = existingChapterInfoId || System.createUniqueId();
return ChapterRepo.insertChapterInformation(chapterInfoId, userId, chapterId, actId, bookId, plotId, incidentId, lang); 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 { public static updateChapter(userId: string, chapterId: string, title: string, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): boolean {
const hashedTitle: string = System.hashElement(title); const hashedTitle: string = System.hashElement(title);
const userKey: string = getUserEncryptionKey(userId); const userEncryptionKey: string = getUserEncryptionKey(userId);
const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey); const encryptedTitle: string = System.encryptDataWithUserKey(title, userEncryptionKey);
return ChapterRepo.updateChapter(userId, chapterId, encryptedTitle, hashedTitle, chapterOrder, System.timeStampInSeconds(), lang); 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); * Updates chapter information for multiple chapters including summary and goal.
for (const chapter of chapters) { * @param chapters - Array of ActChapter objects containing updated information
const summary: string = chapter.summary ? System.encryptDataWithUserKey(chapter.summary, userKey) : ''; * @param userId - The unique identifier of the user
const goal: string = chapter.goal ? System.encryptDataWithUserKey(chapter.goal, userKey) : ''; * @param actId - The act number the chapters belong to
const chapterId: string = chapter.chapterId; * @param bookId - The unique identifier of the book
ChapterRepo.updateChapterInfos(userId, chapterId, actId, bookId, incidentId, plotId, summary, goal, System.timeStampInSeconds(), lang); * @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 { static getCompanionContent(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): CompanionContent {
const versionNum: number = version - 1; const companionVersion: number = version - 1;
const chapterResponse: CompanionContentQueryResult[] = ChapterRepo.fetchCompanionContent(userId, chapterId, versionNum, lang); const companionContentResults: CompanionContentQueryResult[] = ChapterContentRepository.fetchCompanionContent(userId, chapterId, companionVersion, lang);
if (chapterResponse.length === 0) {
if (companionContentResults.length === 0) {
return { return {
version: version, version: version,
content: '', content: '',
wordsCount: 0 wordsCount: 0
}; };
} }
const chapter: CompanionContentQueryResult = chapterResponse[0];
const userKey: string = getUserEncryptionKey(userId); const companionContentData: CompanionContentQueryResult = companionContentResults[0];
const userEncryptionKey: string = getUserEncryptionKey(userId);
return { return {
version: chapter.version, version: companionContentData.version,
content: chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '', content: companionContentData.content ? System.decryptDataWithUserKey(companionContentData.content, userEncryptionKey) : '',
wordsCount: chapter.words_count 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[] { static getChapterStory(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): ActStory[] {
const stories: ChapterStoryQueryResult[] = ChapterRepo.fetchChapterStory(userId, chapterId, lang); const chapterStoryResults: ChapterStoryQueryResult[] = ChapterRepo.fetchChapterStory(userId, chapterId, lang);
const actStories: Record<number, ActStory> = {}; const actStoriesMap: Record<number, ActStory> = {};
const userKey: string = getUserEncryptionKey(userId); const userEncryptionKey: string = getUserEncryptionKey(userId);
for (const story of stories) { for (const storyResult of chapterStoryResults) {
const actId: number = story.act_id; const actId: number = storyResult.act_id;
if (!actStories[actId]) { if (!actStoriesMap[actId]) {
actStories[actId] = { actStoriesMap[actId] = {
actId: actId, actId: actId,
summary: story.summary ? System.decryptDataWithUserKey(story.summary, userKey) : '', summary: storyResult.summary ? System.decryptDataWithUserKey(storyResult.summary, userEncryptionKey) : '',
chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '', chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '',
chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : '', chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : '',
incidents: [], incidents: [],
plotPoints: [] plotPoints: []
}; };
} }
if (story.incident_id) { if (storyResult.incident_id) {
const incidentTitle = story.incident_title ? System.decryptDataWithUserKey(story.incident_title, userKey) : ''; const decryptedIncidentTitle: string = storyResult.incident_title ? System.decryptDataWithUserKey(storyResult.incident_title, userEncryptionKey) : '';
const incidentSummary = story.incident_summary ? System.decryptDataWithUserKey(story.incident_summary, userKey) : ''; const decryptedIncidentSummary: string = storyResult.incident_summary ? System.decryptDataWithUserKey(storyResult.incident_summary, userEncryptionKey) : '';
const incidentExists = actStories[actId].incidents.some( const incidentAlreadyExists: boolean = actStoriesMap[actId].incidents.some(
(incident) => incident.incidentTitle === incidentTitle && incident.incidentSummary === incidentSummary (existingIncident) => existingIncident.incidentTitle === decryptedIncidentTitle && existingIncident.incidentSummary === decryptedIncidentSummary
); );
if (!incidentExists) { if (!incidentAlreadyExists) {
actStories[actId].incidents.push({ actStoriesMap[actId].incidents.push({
incidentTitle: incidentTitle, incidentTitle: decryptedIncidentTitle,
incidentSummary: incidentSummary, incidentSummary: decryptedIncidentSummary,
chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '', chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '',
chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : '' chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : ''
}); });
} }
} }
if (story.plot_point_id) { if (storyResult.plot_point_id) {
const plotTitle = story.plot_title ? System.decryptDataWithUserKey(story.plot_title, userKey) : ''; const decryptedPlotTitle: string = storyResult.plot_title ? System.decryptDataWithUserKey(storyResult.plot_title, userEncryptionKey) : '';
const plotSummary = story.plot_summary ? System.decryptDataWithUserKey(story.plot_summary, userKey) : ''; const decryptedPlotSummary: string = storyResult.plot_summary ? System.decryptDataWithUserKey(storyResult.plot_summary, userEncryptionKey) : '';
const plotPointExists = actStories[actId].plotPoints.some( const plotPointAlreadyExists: boolean = actStoriesMap[actId].plotPoints.some(
(plotPoint) => plotPoint.plotTitle === plotTitle && plotPoint.plotSummary === plotSummary (existingPlotPoint) => existingPlotPoint.plotTitle === decryptedPlotTitle && existingPlotPoint.plotSummary === decryptedPlotSummary
); );
if (!plotPointExists) { if (!plotPointAlreadyExists) {
actStories[actId].plotPoints.push({ actStoriesMap[actId].plotPoints.push({
plotTitle: plotTitle, plotTitle: decryptedPlotTitle,
plotSummary: plotSummary, plotSummary: decryptedPlotSummary,
chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '', chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '',
chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : '' 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 { * Retrieves the content of a specific chapter version.
const chapter: ContentQueryResult = ChapterRepo.fetchChapterContentByVersion(userId, chapterid, version, lang); * @param userId - The unique identifier of the user
const userKey: string = getUserEncryptionKey(userId); * @param chapterId - The unique identifier of the chapter
return chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : ''; * @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); 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 { static tipTapToHtml(tipTapContent: JSON): string {
interface TipTapNode { const escapeHtmlCharacters = (text: string): string => {
type?: string;
text?: string;
content?: TipTapNode[];
attrs?: Record<string, unknown>;
marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;
}
const escapeHtml = (text: string): string => {
return text return text
.replace(/&/g, '&amp;') .replace(/&/g, '&amp;')
.replace(/</g, '&lt;') .replace(/</g, '&lt;')
@@ -317,75 +461,132 @@ export default class Chapter {
.replace(/'/g, '&#039;'); .replace(/'/g, '&#039;');
}; };
const renderMarks = (text: string, marks?: Array<{ type: string; attrs?: Record<string, unknown> }>): string => { const renderTextWithMarks = (text: string, marks?: TipTapMark[]): string => {
if (!marks || marks.length === 0) return escapeHtml(text); if (!marks || marks.length === 0) return escapeHtmlCharacters(text);
let result = escapeHtml(text); let renderedText: string = escapeHtmlCharacters(text);
marks.forEach((mark) => {
marks.forEach((mark: TipTapMark) => {
switch (mark.type) { switch (mark.type) {
case 'bold': case 'bold':
result = `<strong>${result}</strong>`; renderedText = `<strong>${renderedText}</strong>`;
break; break;
case 'italic': case 'italic':
result = `<em>${result}</em>`; renderedText = `<em>${renderedText}</em>`;
break; break;
case 'underline': case 'underline':
result = `<u>${result}</u>`; renderedText = `<u>${renderedText}</u>`;
break; break;
case 'strike': case 'strike':
result = `<s>${result}</s>`; renderedText = `<s>${renderedText}</s>`;
break; break;
case 'code': case 'code':
result = `<code>${result}</code>`; renderedText = `<code>${renderedText}</code>`;
break; break;
case 'link': case 'link':
const href = mark.attrs?.href || '#'; const linkHref: string = (mark.attrs?.href as string) || '#';
result = `<a href="${escapeHtml(String(href))}">${result}</a>`; renderedText = `<a href="${escapeHtmlCharacters(linkHref)}">${renderedText}</a>`;
break; break;
} }
}); });
return result;
return renderedText;
}; };
const renderNode = (node: TipTapNode): string => { const renderTipTapNode = (node: TipTapNode): string => {
if (!node) return ''; if (!node) return '';
if (node.type === 'text') { if (node.type === 'text') {
const textContent = node.text || '\u00A0'; const textContent: string = node.text || '\u00A0';
return renderMarks(textContent, node.marks); return renderTextWithMarks(textContent, node.marks);
} }
const children = node.content?.map(renderNode).join('') || ''; const childrenHtml: string = node.content?.map(renderTipTapNode).join('') || '';
const textAlign = node.attrs?.textAlign ? ` style="text-align: ${node.attrs.textAlign}"` : ''; const textAlignStyle: string = node.attrs?.textAlign ? ` style="text-align: ${node.attrs.textAlign}"` : '';
switch (node.type) { switch (node.type) {
case 'doc': case 'doc':
return children; return childrenHtml;
case 'paragraph': case 'paragraph':
return `<p${textAlign}>${children || '\u00A0'}</p>`; return `<p${textAlignStyle}>${childrenHtml || '\u00A0'}</p>`;
case 'heading': case 'heading':
const level = node.attrs?.level || 1; const headingLevel: number = (node.attrs?.level as number) || 1;
return `<h${level}${textAlign}>${children}</h${level}>`; return `<h${headingLevel}${textAlignStyle}>${childrenHtml}</h${headingLevel}>`;
case 'bulletList': case 'bulletList':
return `<ul>${children}</ul>`; return `<ul>${childrenHtml}</ul>`;
case 'orderedList': case 'orderedList':
return `<ol>${children}</ol>`; return `<ol>${childrenHtml}</ol>`;
case 'listItem': case 'listItem':
return `<li>${children}</li>`; return `<li>${childrenHtml}</li>`;
case 'blockquote': case 'blockquote':
return `<blockquote>${children}</blockquote>`; return `<blockquote>${childrenHtml}</blockquote>`;
case 'codeBlock': case 'codeBlock':
return `<pre><code>${children}</code></pre>`; return `<pre><code>${childrenHtml}</code></pre>`;
case 'hardBreak': case 'hardBreak':
return '<br />'; return '<br />';
case 'horizontalRule': case 'horizontalRule':
return '<hr />'; return '<hr />';
default: default:
return children; return childrenHtml;
} }
}; };
const contentNode = tipTapContent as unknown as TipTapNode; const contentNode: TipTapNode = tipTapContent as unknown as TipTapNode;
return renderNode(contentNode); 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;
} }
} }

View File

@@ -65,50 +65,81 @@ export interface CharacterAttribute {
values: Attribute[]; 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 { 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[] { public static getCharacterList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): CharacterProps[] {
const userKey: string = getUserEncryptionKey(userId); const userEncryptionKey: string = getUserEncryptionKey(userId);
const characters: CharacterResult[] = CharacterRepo.fetchCharacters(userId, bookId, lang); const encryptedCharacters: CharacterResult[] = CharacterRepo.fetchCharacters(userId, bookId, lang);
if (!characters) return []; if (!encryptedCharacters) return [];
if (characters.length === 0) return []; if (encryptedCharacters.length === 0) return [];
const characterList: CharacterProps[] = []; const decryptedCharacterList: CharacterProps[] = [];
for (const character of characters) { for (const encryptedCharacter of encryptedCharacters) {
characterList.push({ decryptedCharacterList.push({
id: character.character_id, id: encryptedCharacter.character_id,
name: character.first_name ? System.decryptDataWithUserKey(character.first_name, userKey) : '', name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '',
lastName: character.last_name ? System.decryptDataWithUserKey(character.last_name, userKey) : '', lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '',
title: character.title ? System.decryptDataWithUserKey(character.title, userKey) : '', title: encryptedCharacter.title ? System.decryptDataWithUserKey(encryptedCharacter.title, userEncryptionKey) : '',
category: character.category ? System.decryptDataWithUserKey(character.category, userKey) : '', category: encryptedCharacter.category ? System.decryptDataWithUserKey(encryptedCharacter.category, userEncryptionKey) : '',
image: character.image ? System.decryptDataWithUserKey(character.image, userKey) : '', image: encryptedCharacter.image ? System.decryptDataWithUserKey(encryptedCharacter.image, userEncryptionKey) : '',
role: character.role ? System.decryptDataWithUserKey(character.role, userKey) : '', role: encryptedCharacter.role ? System.decryptDataWithUserKey(encryptedCharacter.role, userEncryptionKey) : '',
biography: character.biography ? System.decryptDataWithUserKey(character.biography, userKey) : '', biography: encryptedCharacter.biography ? System.decryptDataWithUserKey(encryptedCharacter.biography, userEncryptionKey) : '',
history: character.history ? System.decryptDataWithUserKey(character.history, userKey) : '', 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 { 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 characterId: string = existingCharacterId || System.createUniqueId();
const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey); const encryptedName: string = System.encryptDataWithUserKey(character.name, userEncryptionKey);
const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userKey); const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userEncryptionKey);
const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userKey); const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userEncryptionKey);
const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey); const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey);
const encryptedImage: string = System.encryptDataWithUserKey(character.image, userKey); const encryptedImage: string = System.encryptDataWithUserKey(character.image, userEncryptionKey);
const encryptedRole: string = System.encryptDataWithUserKey(character.role, userKey); const encryptedRole: string = System.encryptDataWithUserKey(character.role, userEncryptionKey);
const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userKey); const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userEncryptionKey);
const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userKey); const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userEncryptionKey);
CharacterRepo.addNewCharacter(userId, characterId, encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, bookId, lang); CharacterRepo.addNewCharacter(userId, characterId, encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, bookId, lang);
const attributes: string[] = Object.keys(character); const characterPropertyKeys: string[] = Object.keys(character);
for (const key of attributes) { for (const propertyKey of characterPropertyKeys) {
if (Array.isArray(character[key as keyof CharacterPropsPost])) { if (Array.isArray(character[propertyKey as keyof CharacterPropsPost])) {
const array = character[key as keyof CharacterPropsPost] as { name: string }[]; const attributeArray = character[propertyKey as keyof CharacterPropsPost] as { name: string }[];
if (array.length > 0) { if (attributeArray.length > 0) {
for (const item of array) { for (const attributeItem of attributeArray) {
const type: string = key; const attributeType: string = propertyKey;
const name: string = item.name; const attributeName: string = attributeItem.name;
this.addNewAttribute(characterId, userId, type, name, lang); this.addNewAttribute(characterId, userId, attributeType, attributeName, lang);
} }
} }
} }
@@ -116,85 +147,128 @@ export default class Character {
return characterId; 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 { static updateCharacter(userId: string, character: CharacterPropsPost, lang: 'fr' | 'en' = 'fr'): boolean {
const userKey: string = getUserEncryptionKey(userId); const userEncryptionKey: string = getUserEncryptionKey(userId);
if (!character.id) { if (!character.id) {
return false; return false;
} }
const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey); const encryptedName: string = System.encryptDataWithUserKey(character.name, userEncryptionKey);
const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userKey); const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userEncryptionKey);
const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userKey); const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userEncryptionKey);
const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey); const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey);
const encryptedImage: string = System.encryptDataWithUserKey(character.image, userKey); const encryptedImage: string = System.encryptDataWithUserKey(character.image, userEncryptionKey);
const encryptedRole: string = System.encryptDataWithUserKey(character.role, userKey); const encryptedRole: string = System.encryptDataWithUserKey(character.role, userEncryptionKey);
const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userKey); const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userEncryptionKey);
const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userKey); 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); 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 { 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 attributeId: string = existingAttributeId || System.createUniqueId();
const encryptedType: string = System.encryptDataWithUserKey(type, userKey); const encryptedType: string = System.encryptDataWithUserKey(type, userEncryptionKey);
const encryptedName: string = System.encryptDataWithUserKey(name, userKey); const encryptedName: string = System.encryptDataWithUserKey(name, userEncryptionKey);
return CharacterRepo.insertAttribute(attributeId, characterId, userId, encryptedType, encryptedName, lang); 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); 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[] { static getAttributes(characterId: string, userId: string, lang: 'fr' | 'en' = 'fr'): CharacterAttribute[] {
const userKey: string = getUserEncryptionKey(userId); const userEncryptionKey: string = getUserEncryptionKey(userId);
const attributes: AttributeResult[] = CharacterRepo.fetchAttributes(characterId, userId, lang); const encryptedAttributes: AttributeResult[] = CharacterRepo.fetchAttributes(characterId, userId, lang);
if (!attributes?.length) return []; if (!encryptedAttributes?.length) return [];
const groupedMap: Map<string, Attribute[]> = new Map<string, Attribute[]>(); const attributesByType: Map<string, Attribute[]> = new Map<string, Attribute[]>();
for (const attribute of attributes) { for (const encryptedAttribute of encryptedAttributes) {
const type: string = System.decryptDataWithUserKey(attribute.attribute_name, userKey); const decryptedType: string = System.decryptDataWithUserKey(encryptedAttribute.attribute_name, userEncryptionKey);
const value: string = attribute.attribute_value ? System.decryptDataWithUserKey(attribute.attribute_value, userKey) : ''; const decryptedValue: string = encryptedAttribute.attribute_value ? System.decryptDataWithUserKey(encryptedAttribute.attribute_value, userEncryptionKey) : '';
if (!groupedMap.has(type)) { if (!attributesByType.has(decryptedType)) {
groupedMap.set(type, []); attributesByType.set(decryptedType, []);
} }
groupedMap.get(type)!.push({ attributesByType.get(decryptedType)!.push({
id: attribute.attr_id, id: encryptedAttribute.attr_id,
name: value name: decryptedValue
}); });
} }
return Array.from<[string, Attribute[]], CharacterAttribute>( return Array.from<[string, Attribute[]], CharacterAttribute>(
groupedMap, attributesByType,
([type, values]: [string, Attribute[]]): CharacterAttribute => ({type, values}) ([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[] { 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 []; return [];
} }
const userKey: string = getUserEncryptionKey(userId); const userEncryptionKey: string = getUserEncryptionKey(userId);
const completeCharactersMap = new Map<string, CompleteCharacterProps>(); const completeCharactersMap = new Map<string, CompleteCharacterProps>();
for (const character of characterList) { for (const encryptedCharacter of encryptedCharacterList) {
if (!character.character_id) { if (!encryptedCharacter.character_id) {
continue; continue;
} }
if (!completeCharactersMap.has(character.character_id)) { if (!completeCharactersMap.has(encryptedCharacter.character_id)) {
const personnageObj: CompleteCharacterProps = { const decryptedCharacter: CompleteCharacterProps = {
id: '', id: '',
name: character.first_name ? System.decryptDataWithUserKey(character.first_name, userKey) : '', name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '',
lastName: character.last_name ? System.decryptDataWithUserKey(character.last_name, userKey) : '', lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '',
title: character.title ? System.decryptDataWithUserKey(character.title, userKey) : '', title: encryptedCharacter.title ? System.decryptDataWithUserKey(encryptedCharacter.title, userEncryptionKey) : '',
category: character.category ? System.decryptDataWithUserKey(character.category, userKey) : '', category: encryptedCharacter.category ? System.decryptDataWithUserKey(encryptedCharacter.category, userEncryptionKey) : '',
role: character.role ? System.decryptDataWithUserKey(character.role, userKey) : '', role: encryptedCharacter.role ? System.decryptDataWithUserKey(encryptedCharacter.role, userEncryptionKey) : '',
biography: character.biography ? System.decryptDataWithUserKey(character.biography, userKey) : '', biography: encryptedCharacter.biography ? System.decryptDataWithUserKey(encryptedCharacter.biography, userEncryptionKey) : '',
history: character.history ? System.decryptDataWithUserKey(character.history, userKey) : '', history: encryptedCharacter.history ? System.decryptDataWithUserKey(encryptedCharacter.history, userEncryptionKey) : '',
physical: [], physical: [],
psychological: [], psychological: [],
relations: [], relations: [],
@@ -204,36 +278,42 @@ export default class Character {
goals: [], goals: [],
motivations: [] 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; continue;
} }
const decryptedName: string = System.decryptDataWithUserKey(character.attribute_name, userKey); const decryptedAttributeName: string = System.decryptDataWithUserKey(encryptedCharacter.attribute_name, userEncryptionKey);
const decryptedValue: string = character.attribute_value ? System.decryptDataWithUserKey(character.attribute_value, userKey) : ''; const decryptedAttributeValue: string = encryptedCharacter.attribute_value ? System.decryptDataWithUserKey(encryptedCharacter.attribute_value, userEncryptionKey) : '';
if (Array.isArray(personnage[decryptedName])) { if (Array.isArray(characterEntry[decryptedAttributeName])) {
personnage[decryptedName].push({ characterEntry[decryptedAttributeName].push({
id: '', id: '',
name: decryptedValue name: decryptedAttributeValue
}); });
} }
} }
return Array.from(completeCharactersMap.values()); 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 { static characterVCard(characters: CompleteCharacterProps[]): string {
const charactersMap = new Map<string, CompleteCharacterProps>(); const uniqueCharactersMap = new Map<string, CompleteCharacterProps>();
let charactersDescription: string = ''; let formattedCharactersDescription: string = '';
characters.forEach((character: CompleteCharacterProps): void => { 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)) { if (!uniqueCharactersMap.has(characterIdentifier)) {
charactersMap.set(characterKey, { uniqueCharactersMap.set(characterIdentifier, {
name: character.name, name: character.name,
lastName: character.lastName, lastName: character.lastName,
category: character.category, 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 => { Object.keys(character).forEach((propertyName: string): void => {
if (Array.isArray(character[fieldName])) { if (Array.isArray(character[propertyName])) {
if (!characterData[fieldName]) characterData[fieldName] = []; if (!aggregatedCharacterData[propertyName]) aggregatedCharacterData[propertyName] = [];
(characterData[fieldName] as Attribute[]).push(...(character[fieldName] as Attribute[])); (aggregatedCharacterData[propertyName] as Attribute[]).push(...(character[propertyName] as Attribute[]));
} }
}); });
}); });
charactersDescription = Array.from(charactersMap.values()).map((character: CompleteCharacterProps): string => { formattedCharactersDescription = Array.from(uniqueCharactersMap.values()).map((character: CompleteCharacterProps): string => {
const descriptionFields: string[] = []; const characterDescriptionLines: string[] = [];
const fullName: string = [character.name, character.lastName].filter(Boolean).join(' '); 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) => { (['category', 'title', 'role', 'biography', 'history'] as const).forEach((propertyKey) => {
if (character[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]; const propertyValue: string | Attribute[] | undefined = character[propertyKey];
if (Array.isArray(propertyValue) && propertyValue.length > 0) { if (Array.isArray(propertyValue) && propertyValue.length > 0) {
const capitalizedPropertyKey: string = propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1); const capitalizedPropertyKey: string = propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1);
const formattedValues: string = propertyValue.map((item: Attribute) => item.name).join(', '); const formattedAttributeValues: string = propertyValue.map((attributeItem: Attribute) => attributeItem.name).join(', ');
descriptionFields.push(`${capitalizedPropertyKey} : ${formattedValues}`); characterDescriptionLines.push(`${capitalizedPropertyKey} : ${formattedAttributeValues}`);
} }
}); });
return descriptionFields.join('\n'); return characterDescriptionLines.join('\n');
}).join('\n\n'); }).join('\n\n');
return charactersDescription; return formattedCharactersDescription;
} }
} }

View File

@@ -1,3 +1,6 @@
/**
* Represents a TipTap editor node structure.
*/
export interface TiptapNode { export interface TiptapNode {
type: string; type: string;
content?: TiptapNode[]; 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 { 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 { static convertTipTapRawToText(content: string): string {
const text: string = this.convertTiptapToHTMLFromString(content); const htmlContent: string = this.convertTiptapToHTMLFromString(content);
return this.htmlToText(text); 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 return html
.replace(/<br\s*\/?>/gi, '\n') // Gérer les <br> d'abord .replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/?(p|h[1-6]|div)(\s+[^>]*)?>/gi, '\n') // Balises bloc .replace(/<\/?(p|h[1-6]|div)(\s+[^>]*)?>/gi, '\n')
.replace(/<\/?[^>]+(>|$)/g, '') // Supprimer toutes les balises restantes .replace(/<\/?[^>]+(>|$)/g, '')
.replace(/(\n\s*){2,}/g, '\n\n') // Préserver les paragraphes .replace(/(\n\s*){2,}/g, '\n\n')
.replace(/^\s+|\s+$|(?<=\s)\s+/g, '') // Nettoyer les espaces .replace(/^\s+|\s+$|(?<=\s)\s+/g, '')
.trim(); .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 { static convertTiptapToHTMLFromString(jsonString: string): string {
// Convert the JSON string to an object let tiptapNode: TiptapNode;
let jsonObject: TiptapNode;
try { try {
jsonObject = JSON.parse(jsonString); tiptapNode = JSON.parse(jsonString);
} catch (error) { } catch (error) {
console.error('Invalid JSON string:', error); console.error('Invalid JSON string:', error);
return ''; return '';
} }
// Use the existing conversion function return this.convertTiptapToHTML(tiptapNode);
return this.convertTiptapToHTML(jsonObject);
} }
/**
* 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 { static convertTiptapToHTML(node: TiptapNode): string {
let html = ''; let html = '';
switch (node.type) { switch (node.type) {
case 'doc': case 'doc':
if (node.content) { if (node.content) {
node.content.forEach(childNode => { node.content.forEach((childNode: TiptapNode) => {
html += this.convertTiptapToHTML(childNode); html += this.convertTiptapToHTML(childNode);
}); });
} }
@@ -52,7 +86,7 @@ export default class Content {
case 'paragraph': case 'paragraph':
html += '<p>'; html += '<p>';
if (node.content) { if (node.content) {
node.content.forEach(childNode => { node.content.forEach((childNode: TiptapNode) => {
html += this.convertTiptapToHTML(childNode); html += this.convertTiptapToHTML(childNode);
}); });
} }
@@ -60,45 +94,44 @@ export default class Content {
break; break;
case 'text': case 'text':
let textContent = node.text || ''; let formattedText = node.text || '';
// Apply attributes like bold, italic, etc.
if (node.attrs) { if (node.attrs) {
if (node.attrs.bold) { if (node.attrs.bold) {
textContent = `<strong>${textContent}</strong>`; formattedText = `<strong>${formattedText}</strong>`;
} }
if (node.attrs.italic) { if (node.attrs.italic) {
textContent = `<em>${textContent}</em>`; formattedText = `<em>${formattedText}</em>`;
} }
if (node.attrs.underline) { if (node.attrs.underline) {
textContent = `<u>${textContent}</u>`; formattedText = `<u>${formattedText}</u>`;
} }
if (node.attrs.strike) { if (node.attrs.strike) {
textContent = `<s>${textContent}</s>`; formattedText = `<s>${formattedText}</s>`;
} }
if (node.attrs.link) { if (node.attrs.link) {
textContent = `<a href="${node.attrs.link.href}">${textContent}</a>`; formattedText = `<a href="${node.attrs.link.href}">${formattedText}</a>`;
} }
} }
html += textContent; html += formattedText;
break; break;
case 'heading': case 'heading':
const level = node.attrs?.level || 1; const headingLevel = node.attrs?.level || 1;
html += `<h${level}>`; html += `<h${headingLevel}>`;
if (node.content) { if (node.content) {
node.content.forEach(childNode => { node.content.forEach((childNode: TiptapNode) => {
html += this.convertTiptapToHTML(childNode); html += this.convertTiptapToHTML(childNode);
}); });
} }
html += `</h${level}>`; html += `</h${headingLevel}>`;
break; break;
case 'bulletList': case 'bulletList':
html += '<ul>'; html += '<ul>';
if (node.content) { if (node.content) {
node.content.forEach(childNode => { node.content.forEach((childNode: TiptapNode) => {
html += this.convertTiptapToHTML(childNode); html += this.convertTiptapToHTML(childNode);
}); });
} }
@@ -108,7 +141,7 @@ export default class Content {
case 'orderedList': case 'orderedList':
html += '<ol>'; html += '<ol>';
if (node.content) { if (node.content) {
node.content.forEach(childNode => { node.content.forEach((childNode: TiptapNode) => {
html += this.convertTiptapToHTML(childNode); html += this.convertTiptapToHTML(childNode);
}); });
} }
@@ -118,7 +151,7 @@ export default class Content {
case 'listItem': case 'listItem':
html += '<li>'; html += '<li>';
if (node.content) { if (node.content) {
node.content.forEach(childNode => { node.content.forEach((childNode: TiptapNode) => {
html += this.convertTiptapToHTML(childNode); html += this.convertTiptapToHTML(childNode);
}); });
} }
@@ -128,7 +161,7 @@ export default class Content {
case 'blockquote': case 'blockquote':
html += '<blockquote>'; html += '<blockquote>';
if (node.content) { if (node.content) {
node.content.forEach(childNode => { node.content.forEach((childNode: TiptapNode) => {
html += this.convertTiptapToHTML(childNode); html += this.convertTiptapToHTML(childNode);
}); });
} }
@@ -138,7 +171,7 @@ export default class Content {
case 'codeBlock': case 'codeBlock':
html += '<pre><code>'; html += '<pre><code>';
if (node.content) { if (node.content) {
node.content.forEach(childNode => { node.content.forEach((childNode: TiptapNode) => {
html += this.convertTiptapToHTML(childNode); html += this.convertTiptapToHTML(childNode);
}); });
} }
@@ -148,7 +181,7 @@ export default class Content {
default: default:
console.warn(`Unhandled node type: ${node.type}`); console.warn(`Unhandled node type: ${node.type}`);
if (node.content) { if (node.content) {
node.content.forEach(childNode => { node.content.forEach((childNode: TiptapNode) => {
html += this.convertTiptapToHTML(childNode); html += this.convertTiptapToHTML(childNode);
}); });
} }

View File

@@ -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<string> {
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<boolean> {
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 '';
}
}
}

View File

@@ -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<boolean> {
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);
});
}
}

View File

@@ -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-size: 24px !important;
font-weight: bold !important; font-weight: bold !important;
text-indent: 24px !important; text-indent: 24px !important;

View File

@@ -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<GuideLineProps | null> {
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<boolean> {
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
);
}
}

View File

@@ -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<IncidentProps[]> {
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);
}
}

View File

@@ -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<IssueProps[]> {
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);
}
}

View File

@@ -25,22 +25,42 @@ export interface LocationProps {
elements: Element[]; 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 { export default class Location {
/** /**
* Récupère toutes les locations pour un utilisateur et un livre donnés. * Retrieves all locations for a given user and book.
* @param {string} userId - L'ID de l'utilisateur. * @param userId - The user's unique identifier.
* @param {string} bookId - L'ID du livre. * @param bookId - The book's unique identifier.
* @returns {LocationProps[]} - Un tableau de propriétés de location. * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
* @throws {Error} - Lance une erreur si une exception se produit lors de la récupération des locations. * @returns An array of location properties with their elements and sub-elements.
*/ */
static getAllLocations(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LocationProps[] { static getAllLocations(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LocationProps[] {
const locations: LocationQueryResult[] = LocationRepo.getLocation(userId, bookId, lang); const locationRecords: LocationQueryResult[] = LocationRepo.getLocation(userId, bookId, lang);
if (!locations || locations.length === 0) return []; if (!locationRecords || locationRecords.length === 0) return [];
const userKey: string = getUserEncryptionKey(userId); const userKey: string = getUserEncryptionKey(userId);
const locationArray: LocationProps[] = []; const locationArray: LocationProps[] = [];
for (const record of locations) { for (const record of locationRecords) {
let location = locationArray.find(loc => loc.id === record.loc_id); let location = locationArray.find(loc => loc.id === record.loc_id);
if (!location) { if (!location) {
@@ -57,12 +77,12 @@ export default class Location {
let element = location.elements.find(elem => elem.id === record.element_id); let element = location.elements.find(elem => elem.id === record.element_id);
if (!element) { if (!element) {
const decryptedName: string = System.decryptDataWithUserKey(record.element_name, userKey); 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 = { element = {
id: record.element_id, id: record.element_id,
name: decryptedName, name: decryptedName,
description: decryptedDesc, description: decryptedDescription,
subElements: [] subElements: []
}; };
location.elements.push(element); location.elements.push(element);
@@ -73,13 +93,12 @@ export default class Location {
if (!subElementExists) { if (!subElementExists) {
const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, userKey); 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({ element.subElements.push({
id: record.sub_element_id, id: record.sub_element_id,
name: decryptedName, name: decryptedName,
description: decryptedDesc description: decryptedDescription
}); });
} }
} }
@@ -88,47 +107,81 @@ export default class Location {
return locationArray; 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 { static addLocationSection(userId: string, locationName: string, bookId: string, lang: 'fr' | 'en' = 'fr', existingLocationId?: string): string {
const userKey: string = getUserEncryptionKey(userId); 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 encryptedName: string = System.encryptDataWithUserKey(locationName, userKey);
const locationId: string = existingLocationId || System.createUniqueId(); 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 userKey: string = getUserEncryptionKey(userId);
const originalName: string = System.hashElement(elementName); const hashedName: string = System.hashElement(elementName);
const encryptedName: string = System.encryptDataWithUserKey(elementName, userKey); const encryptedName: string = System.encryptDataWithUserKey(elementName, userKey);
const elementId: string = existingElementId || System.createUniqueId(); 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 userKey: string = getUserEncryptionKey(userId);
const originalName: string = System.hashElement(subElementName); const hashedName: string = System.hashElement(subElementName);
const encryptedName: string = System.encryptDataWithUserKey(subElementName, userKey); const encryptedName: string = System.encryptDataWithUserKey(subElementName, userKey);
const subElementId: string = existingSubElementId || System.createUniqueId(); 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); const userKey: string = getUserEncryptionKey(userId);
for (const location of locations) { for (const location of locations) {
const originalName: string = System.hashElement(location.name); const hashedLocationName: string = System.hashElement(location.name);
const encryptedName: string = System.encryptDataWithUserKey(location.name, userKey); const encryptedLocationName: string = System.encryptDataWithUserKey(location.name, userKey);
LocationRepo.updateLocationSection(userId, location.id, encryptedName, originalName, System.timeStampInSeconds(),lang) LocationRepo.updateLocationSection(userId, location.id, encryptedLocationName, hashedLocationName, System.timeStampInSeconds(), lang)
for (const element of location.elements) { for (const element of location.elements) {
const originalName: string = System.hashElement(element.name); const hashedElementName: string = System.hashElement(element.name);
const encryptedName: string = System.encryptDataWithUserKey(element.name, userKey); const encryptedElementName: string = System.encryptDataWithUserKey(element.name, userKey);
const encryptDescription: string = element.description ? System.encryptDataWithUserKey(element.description, userKey) : ''; const encryptedElementDescription: string = element.description ? System.encryptDataWithUserKey(element.description, userKey) : '';
LocationRepo.updateLocationElement(userId, element.id, encryptedName, originalName, encryptDescription, System.timeStampInSeconds(), lang) LocationRepo.updateLocationElement(userId, element.id, encryptedElementName, hashedElementName, encryptedElementDescription, System.timeStampInSeconds(), lang)
for (const subElement of element.subElements) { for (const subElement of element.subElements) {
const originalName: string = System.hashElement(subElement.name); const hashedSubElementName: string = System.hashElement(subElement.name);
const encryptedName: string = System.encryptDataWithUserKey(subElement.name, userKey); const encryptedSubElementName: string = System.encryptDataWithUserKey(subElement.name, userKey);
const encryptDescription: string = subElement.description ? System.encryptDataWithUserKey(subElement.description, userKey) : ''; const encryptedSubElementDescription: string = subElement.description ? System.encryptDataWithUserKey(subElement.description, userKey) : '';
LocationRepo.updateLocationSubElement(userId, subElement.id, encryptedName, originalName, encryptDescription,System.timeStampInSeconds(),lang) 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); 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); 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); 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[] { static getLocationTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SubElement[] {
const data: LocationElementQueryResult[] = LocationRepo.fetchLocationTags(userId, bookId, lang); const tagRecords: LocationElementQueryResult[] = LocationRepo.fetchLocationTags(userId, bookId, lang);
if (!data || data.length === 0) return []; if (!tagRecords || tagRecords.length === 0) return [];
const userKey: string = getUserEncryptionKey(userId); const userKey: string = getUserEncryptionKey(userId);
const elementCounts = new Map<string, number>(); const elementCounts = new Map<string, number>();
data.forEach((record: LocationElementQueryResult): void => { tagRecords.forEach((record: LocationElementQueryResult): void => {
elementCounts.set(record.element_id, (elementCounts.get(record.element_id) || 0) + 1); elementCounts.set(record.element_id, (elementCounts.get(record.element_id) || 0) + 1);
}); });
const subElements: SubElement[] = []; const subElements: SubElement[] = [];
const processedIds = new Set<string>(); const processedIds = new Set<string>();
for (const record of data) { for (const record of tagRecords) {
const elementCount: number = elementCounts.get(record.element_id) || 0; const elementCount: number = elementCounts.get(record.element_id) || 0;
if (elementCount > 1 && record.sub_element_id) { if (elementCount > 1 && record.sub_element_id) {
@@ -189,46 +271,58 @@ export default class Location {
return subElements; 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[] { static getLocationsByTags(userId: string, locations: string[], lang: 'fr' | 'en' = 'fr'): Element[] {
const locationsTags: LocationByTagResult[] = LocationRepo.fetchLocationsByTags(userId, locations, lang); const locationTagRecords: LocationByTagResult[] = LocationRepo.fetchLocationsByTags(userId, locations, lang);
if (!locationsTags || locationsTags.length === 0) return []; if (!locationTagRecords || locationTagRecords.length === 0) return [];
const userKey: string = getUserEncryptionKey(userId); const userKey: string = getUserEncryptionKey(userId);
const locationTags: Element[] = []; const locationElements: Element[] = [];
for (const record of locationsTags) { for (const record of locationTagRecords) {
let element: Element | undefined = locationTags.find((elem: Element): boolean => elem.name === record.element_name); let element: Element | undefined = locationElements.find((elem: Element): boolean => elem.name === record.element_name);
if (!element) { if (!element) {
const decryptedName: string = System.decryptDataWithUserKey(record.element_name, userKey); 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 = { element = {
id: '', id: '',
name: decryptedName, name: decryptedName,
description: decryptedDesc, description: decryptedDescription,
subElements: [] subElements: []
}; };
locationTags.push(element); locationElements.push(element);
} }
if (record.sub_elem_name) { if (record.sub_elem_name) {
const subElementExists: boolean = element.subElements.some(sub => sub.name === record.sub_elem_name); const subElementExists: boolean = element.subElements.some(sub => sub.name === record.sub_elem_name);
if (!subElementExists) { if (!subElementExists) {
const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, userKey); 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({ element.subElements.push({
id: '', id: '',
name: decryptedName, 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 { static locationsDescription(locations: Element[]): string {
return locations.map((location: Element): string => { return locations.map((location: Element): string => {
const fields: string[] = []; const descriptionFields: string[] = [];
if (location.name) fields.push(`Nom : ${location.name}`); if (location.name) descriptionFields.push(`Nom : ${location.name}`);
if (location.description) fields.push(`Description : ${location.description}`); if (location.description) descriptionFields.push(`Description : ${location.description}`);
return fields.join('\n'); return descriptionFields.join('\n');
}).join('\n\n'); }).join('\n\n');
} }
} }

View File

@@ -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"; 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 = export type AnthropicModel =
"claude-3-7-sonnet-20250219" "claude-3-7-sonnet-20250219"
| "claude-sonnet-4-20250514" | "claude-sonnet-4-20250514"
@@ -7,6 +14,10 @@ export type AnthropicModel =
| "claude-3-5-sonnet-20241022" | "claude-3-5-sonnet-20241022"
| "claude-3-5-sonnet-20240620" | "claude-3-5-sonnet-20240620"
| "claude-3-opus-20240229"; | "claude-3-opus-20240229";
/**
* Supported Google Gemini model identifiers.
*/
export type GeminiModel = export type GeminiModel =
| "gemini-2.0-flash-001" | "gemini-2.0-flash-001"
| "gemini-2.0-flash-lite-001" | "gemini-2.0-flash-lite-001"
@@ -14,16 +25,30 @@ export type GeminiModel =
| "gemini-2.5-flash-lite" | "gemini-2.5-flash-lite"
| "gemini-2.5-pro"; | "gemini-2.5-pro";
/**
* Configuration object representing an AI model with its pricing information.
*/
export interface AIModelConfig { export interface AIModelConfig {
/** Unique identifier for the AI model */
model_id: string; model_id: string;
/** Human-readable display name for the model */
model_name: string; model_name: string;
/** Brand or provider of the model (e.g., Anthropic, OpenAI, Google) */
brand: string; brand: string;
/** Price per input tokens in USD */
price_token_in: number; price_token_in: number;
/** Number of input tokens per price unit */
per_quantity_in: number; per_quantity_in: number;
/** Price per output tokens in USD */
price_token_out: number; price_token_out: number;
/** Number of output tokens per price unit */
per_quantity_out: number; 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[] = [ export const AIModels: AIModelConfig[] = [
{ {
"model_id": "claude-3-5-haiku-20241022", "model_id": "claude-3-5-haiku-20241022",
@@ -250,4 +275,4 @@ export const AIModels: AIModelConfig[] = [
"price_token_out": 0.4, "price_token_out": 0.4,
"per_quantity_out": 1000000 "per_quantity_out": 1000000
} }
] ]

View File

@@ -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<PlotPointProps[]> {
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);
}
}

View File

@@ -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<CompleteBook> {
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<boolean> {
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<SyncedBook[]> {
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
};
});
}
}

View File

@@ -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<CompleteBook> {
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<BookChapterContentTable[]> =>
ChapterContentRepository.fetchBookChapterContents(userId, chapter.chapter_id, lang))),
Promise.all(encryptedChapters.map((chapter: BookChaptersTable): Promise<BookChapterInfosTable[]> =>
ChapterRepo.fetchBookChapterInfos(userId, chapter.chapter_id, lang))),
Promise.all(encryptedCharacters.map((character: BookCharactersTable): Promise<BookCharactersAttributesTable[]> =>
CharacterRepo.fetchBookCharactersAttributes(userId, character.character_id, lang))),
Promise.all(encryptedWorlds.map((world: BookWorldTable): Promise<BookWorldElementsTable[]> =>
WorldRepository.fetchBookWorldElements(userId, world.world_id, lang))),
Promise.all(encryptedLocations.map((location: BookLocationTable): Promise<LocationElementTable[]> =>
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<LocationSubElementTable[]> =>
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
};
}
}

View File

@@ -3,24 +3,36 @@ import System from "../System.js";
import Book, {BookProps} from "./Book.js"; import Book, {BookProps} from "./Book.js";
import {getUserEncryptionKey} from "../keyManager.js"; import {getUserEncryptionKey} from "../keyManager.js";
interface UserAccount{ /**
firstName:string; * Represents a user account with basic profile information.
lastName:string; */
username:string interface UserAccount {
authorName:string; firstName: string;
email:string; lastName: string;
username: string;
authorName: string;
email: string;
} }
/**
* Represents the guide tour completion status for various features.
*/
export interface GuideTour { export interface GuideTour {
[key: string]: boolean; [key: string]: boolean;
} }
/**
* Summary information for a book associated with a user.
*/
interface BookSummary { interface BookSummary {
bookId: string; bookId: string;
title: string; title: string;
subTitle?: string; subTitle?: string;
} }
/**
* Complete user information response including profile data and associated books.
*/
export interface UserInfoResponse { export interface UserInfoResponse {
id: string; id: string;
name: string; name: string;
@@ -31,13 +43,17 @@ export interface UserInfoResponse {
authorName: string; authorName: string;
groupId: number; groupId: number;
termsAccepted: boolean; termsAccepted: boolean;
guideTour: any[]; guideTour: GuideTour[];
books: BookSummary[]; 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 firstName: string;
private lastName: string; private lastName: string;
private username: string; private username: string;
@@ -47,7 +63,11 @@ export default class User{
private groupId: number; private groupId: number;
private termsAccepted: boolean; 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.id = id;
this.firstName = ''; this.firstName = '';
this.lastName = ''; this.lastName = '';
@@ -58,25 +78,35 @@ export default class User{
this.groupId = 0; this.groupId = 0;
this.termsAccepted = false; 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<void> { public async getUserInfos(): Promise<void> {
const data: UserInfosQueryResponse = UserRepo.fetchUserInfos(this.id); const userInfosData: UserInfosQueryResponse = UserRepo.fetchUserInfos(this.id);
const userKey:string = getUserEncryptionKey(this.id) const userEncryptionKey: string = getUserEncryptionKey(this.id);
this.firstName = System.decryptDataWithUserKey(data.first_name, userKey); this.firstName = System.decryptDataWithUserKey(userInfosData.first_name, userEncryptionKey);
this.lastName = System.decryptDataWithUserKey(data.last_name, userKey); this.lastName = System.decryptDataWithUserKey(userInfosData.last_name, userEncryptionKey);
this.username = System.decryptDataWithUserKey(data.username, userKey); this.username = System.decryptDataWithUserKey(userInfosData.username, userEncryptionKey);
this.email = System.decryptDataWithUserKey(data.email, userKey); this.email = System.decryptDataWithUserKey(userInfosData.email, userEncryptionKey);
this.accountVerified = data.account_verified === 1; this.accountVerified = userInfosData.account_verified === 1;
this.authorName = data.author_name ? System.decryptDataWithUserKey(data.author_name, userKey) : ''; this.authorName = userInfosData.author_name ? System.decryptDataWithUserKey(userInfosData.author_name, userEncryptionKey) : '';
this.groupId = data.user_group ? data.user_group : 0; this.groupId = userInfosData.user_group ? userInfosData.user_group : 0;
this.termsAccepted = data.term_accepted === 1; this.termsAccepted = userInfosData.term_accepted === 1;
} }
public static async returnUserInfos(userId: string):Promise<UserInfoResponse> { /**
* 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<UserInfoResponse> {
const user: User = new User(userId); const user: User = new User(userId);
await user.getUserInfos(); await user.getUserInfos();
const books: BookProps[] = await Book.getBooks(userId); const userBooks: BookProps[] = await Book.getBooks(userId);
const guideTour: GuideTour[] = []; const guideTourStatus: GuideTour[] = [];
return { return {
id: user.getId(), id: user.getId(),
name: user.getFirstName(), name: user.getFirstName(),
@@ -87,95 +117,194 @@ export default class User{
authorName: user.getAuthorName(), authorName: user.getAuthorName(),
groupId: user.getGroupId(), groupId: user.getGroupId(),
termsAccepted: user.isTermsAccepted(), termsAccepted: user.isTermsAccepted(),
guideTour: guideTour, guideTour: guideTourStatus,
books: books.map((book: BookProps):BookSummary => { books: userBooks.map((book: BookProps): BookSummary => {
return { return {
bookId: book.id, bookId: book.id,
title: book.title, title: book.title,
subTitle: book.subTitle, subTitle: book.subTitle,
}; };
}) })
}
}
public static async addUser(userId:string,firstName: string, lastName: string, username: string, email: string, notEncryptPassword: string, lang: 'fr' | 'en' = 'fr'): Promise<string> {
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<boolean> {
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<UserAccount> {
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<string> {
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<boolean> {
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<UserAccount> {
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 { public getId(): string {
return this.id; return this.id;
} }
/**
* Gets the user's first name.
* @returns The user's first name
*/
public getFirstName(): string { public getFirstName(): string {
return this.firstName; return this.firstName;
} }
/**
* Gets the user's last name.
* @returns The user's last name
*/
public getLastName(): string { public getLastName(): string {
return this.lastName; return this.lastName;
} }
/**
* Gets the user's username.
* @returns The user's username
*/
public getUsername(): string { public getUsername(): string {
return this.username; return this.username;
} }
/**
* Gets the user's email address.
* @returns The user's email address
*/
public getEmail(): string { public getEmail(): string {
return this.email; return this.email;
} }
/**
* Checks if the user's account has been verified.
* @returns True if the account is verified, false otherwise
*/
public isAccountVerified(): boolean { public isAccountVerified(): boolean {
return this.accountVerified; 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 { public isTermsAccepted(): boolean {
return this.termsAccepted; return this.termsAccepted;
} }
/**
* Gets the user's group identifier.
* @returns The user's group identifier
*/
public getGroupId(): number { public getGroupId(): number {
return this.groupId; return this.groupId;
} }
/**
* Gets the user's author/pen name.
* @returns The user's author name
*/
public getAuthorName(): string { public getAuthorName(): string {
return this.authorName; return this.authorName;
} }

View File

@@ -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<string, number> = {
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<number, keyof WorldProps> = {
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);
}
}

View File

@@ -33,7 +33,7 @@ export default class IncidentRepository {
* @returns An array of incidents with their ID, title, and summary * @returns An array of incidents with their ID, title, and summary
* @throws Error if the database query fails * @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 { try {
const db: Database = System.getDb(); const db: Database = System.getDb();
const query: string = 'SELECT incident_id, title, summary FROM book_incidents WHERE author_id=? AND book_id=?'; const query: string = 'SELECT incident_id, title, summary FROM book_incidents WHERE author_id=? AND book_id=?';