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:
185
electron/database/models/Act.ts
Normal file
185
electron/database/models/Act.ts
Normal 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
@@ -1,14 +1,18 @@
|
||||
import System from "../System.js";
|
||||
import { getUserEncryptionKey } from "../keyManager.js";
|
||||
import Book, { CompleteBookData } from "./Book.js";
|
||||
import ChapterRepo, {
|
||||
ActChapterQuery,
|
||||
ChapterQueryResult,
|
||||
ChapterContentQueryResult,
|
||||
LastChapterResult,
|
||||
CompanionContentQueryResult,
|
||||
ChapterStoryQueryResult,
|
||||
ContentQueryResult
|
||||
LastChapterResult
|
||||
} from "../repositories/chapter.repository.js";
|
||||
import System from "../System.js";
|
||||
import {getUserEncryptionKey} from "../keyManager.js";
|
||||
import { ActChapter, ActStory } from "./Act.js";
|
||||
import ChapterContentRepository, {
|
||||
ChapterContentQueryResult,
|
||||
CompanionContentQueryResult,
|
||||
ContentQueryResult
|
||||
} from "../repositories/chaptercontent.repository.js";
|
||||
|
||||
export interface ChapterContent {
|
||||
version: number;
|
||||
@@ -28,287 +32,427 @@ export interface ChapterProps {
|
||||
chapterContent?: ChapterContent
|
||||
}
|
||||
|
||||
export interface ActChapter {
|
||||
chapterInfoId: number;
|
||||
chapterId: string;
|
||||
title: string;
|
||||
chapterOrder: number;
|
||||
actId: number;
|
||||
incidentId: string | null;
|
||||
plotPointId: string | null;
|
||||
summary: string;
|
||||
goal: string;
|
||||
}
|
||||
|
||||
export interface CompanionContent {
|
||||
version: number;
|
||||
content: string;
|
||||
wordsCount: number;
|
||||
}
|
||||
|
||||
export interface ActStory {
|
||||
actId: number;
|
||||
summary: string;
|
||||
chapterSummary: string;
|
||||
chapterGoal: string;
|
||||
incidents: IncidentStory[];
|
||||
plotPoints: PlotPointStory[];
|
||||
export interface SyncedChapter {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
contents: SyncedChapterContent[];
|
||||
info: SyncedChapterInfo | null;
|
||||
}
|
||||
|
||||
export interface IncidentStory {
|
||||
incidentTitle: string;
|
||||
incidentSummary: string;
|
||||
chapterSummary: string;
|
||||
chapterGoal: string;
|
||||
export interface SyncedChapterContent {
|
||||
id: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export interface PlotPointStory {
|
||||
plotTitle: string;
|
||||
plotSummary: string;
|
||||
chapterSummary: string;
|
||||
chapterGoal: string;
|
||||
export interface SyncedChapterInfo {
|
||||
id: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export interface CompleteChapterContent {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
order: number;
|
||||
version?: number;
|
||||
}
|
||||
|
||||
interface TipTapNode {
|
||||
type?: string;
|
||||
text?: string;
|
||||
content?: TipTapNode[];
|
||||
attrs?: Record<string, unknown>;
|
||||
marks?: TipTapMark[];
|
||||
}
|
||||
|
||||
interface TipTapMark {
|
||||
type: string;
|
||||
attrs?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export default class Chapter {
|
||||
/**
|
||||
* Retrieves all chapters from a specific book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of ChapterProps containing chapter details
|
||||
*/
|
||||
public static getAllChaptersFromABook(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps[] {
|
||||
const chapters: ChapterQueryResult[] = ChapterRepo.fetchAllChapterFromABook(userId, bookId, lang);
|
||||
let returnChapters: ChapterProps[] = [];
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
for (const chapter of chapters) {
|
||||
const title: string = System.decryptDataWithUserKey(chapter.title, userKey);
|
||||
returnChapters.push({
|
||||
chapterId: chapter.chapter_id,
|
||||
title: title,
|
||||
chapterOrder: chapter.chapter_order
|
||||
const chapterQueryResults: ChapterQueryResult[] = ChapterRepo.fetchAllChapterFromABook(userId, bookId, lang);
|
||||
const decryptedChapters: ChapterProps[] = [];
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
for (const chapterResult of chapterQueryResults) {
|
||||
const decryptedTitle: string = System.decryptDataWithUserKey(chapterResult.title, userEncryptionKey);
|
||||
decryptedChapters.push({
|
||||
chapterId: chapterResult.chapter_id,
|
||||
title: decryptedTitle,
|
||||
chapterOrder: chapterResult.chapter_order
|
||||
});
|
||||
}
|
||||
return returnChapters;
|
||||
|
||||
return decryptedChapters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all chapters organized by acts for a specific book.
|
||||
* Caches decrypted titles to avoid redundant decryption operations.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of ActChapter containing chapter details with act information
|
||||
*/
|
||||
public static getAllChapterFromActs(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ActChapter[] {
|
||||
const query: ActChapterQuery[] = ChapterRepo.fetchAllChapterForActs(userId, bookId, lang);
|
||||
let chapters: ActChapter[] = [];
|
||||
let tempChapter: { id: string, title: string }[] = []
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
if (query.length > 0) {
|
||||
for (const chapter of query) {
|
||||
let decryptTitle: string = '';
|
||||
const newTitleId: number = tempChapter.findIndex((temp: { id: string, title: string }) => temp.id === chapter.chapter_id);
|
||||
if (newTitleId > -1) {
|
||||
decryptTitle = tempChapter[newTitleId]?.title ?? ''
|
||||
} else {
|
||||
decryptTitle = System.decryptDataWithUserKey(chapter.title, userKey);
|
||||
tempChapter.push({id: chapter.chapter_id, title: decryptTitle});
|
||||
}
|
||||
chapters.push({
|
||||
chapterId: chapter.chapter_id,
|
||||
title: decryptTitle,
|
||||
actId: chapter.act_id,
|
||||
chapterInfoId: chapter.chapter_info_id,
|
||||
chapterOrder: chapter.chapter_order,
|
||||
goal: chapter.goal ? System.decryptDataWithUserKey(chapter.goal, userKey) : '',
|
||||
summary: chapter.summary ? System.decryptDataWithUserKey(chapter.summary, userKey) : '',
|
||||
incidentId: chapter.incident_id,
|
||||
plotPointId: chapter.plot_point_id
|
||||
})
|
||||
}
|
||||
return chapters;
|
||||
} else {
|
||||
return []
|
||||
const actChapterQueryResults: ActChapterQuery[] = ChapterRepo.fetchAllChapterForActs(userId, bookId, lang);
|
||||
const actChapters: ActChapter[] = [];
|
||||
const decryptedTitleCache: { id: string; title: string }[] = [];
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
if (actChapterQueryResults.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const chapterQueryResult of actChapterQueryResults) {
|
||||
let decryptedTitle: string = '';
|
||||
const cachedTitleIndex: number = decryptedTitleCache.findIndex(
|
||||
(cachedItem: { id: string; title: string }) => cachedItem.id === chapterQueryResult.chapter_id
|
||||
);
|
||||
|
||||
if (cachedTitleIndex > -1) {
|
||||
decryptedTitle = decryptedTitleCache[cachedTitleIndex]?.title ?? '';
|
||||
} else {
|
||||
decryptedTitle = System.decryptDataWithUserKey(chapterQueryResult.title, userEncryptionKey);
|
||||
decryptedTitleCache.push({ id: chapterQueryResult.chapter_id, title: decryptedTitle });
|
||||
}
|
||||
|
||||
actChapters.push({
|
||||
chapterId: chapterQueryResult.chapter_id,
|
||||
title: decryptedTitle,
|
||||
actId: chapterQueryResult.act_id,
|
||||
chapterInfoId: chapterQueryResult.chapter_info_id,
|
||||
chapterOrder: chapterQueryResult.chapter_order,
|
||||
goal: chapterQueryResult.goal ? System.decryptDataWithUserKey(chapterQueryResult.goal, userEncryptionKey) : '',
|
||||
summary: chapterQueryResult.summary ? System.decryptDataWithUserKey(chapterQueryResult.summary, userEncryptionKey) : '',
|
||||
incidentId: chapterQueryResult.incident_id,
|
||||
plotPointId: chapterQueryResult.plot_point_id
|
||||
});
|
||||
}
|
||||
|
||||
return actChapters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a complete chapter with its content for a specific version.
|
||||
* Optionally updates the last chapter record for the book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param version - The version number of the chapter content
|
||||
* @param bookId - Optional book identifier to update last chapter record
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns ChapterProps containing chapter details and content
|
||||
*/
|
||||
public static getWholeChapter(userId: string, chapterId: string, version: number, bookId?: string, lang: 'fr' | 'en' = 'fr'): ChapterProps {
|
||||
const chapter: ChapterContentQueryResult = ChapterRepo.fetchWholeChapter(userId, chapterId, version, lang);
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const chapterContentResult: ChapterContentQueryResult = ChapterContentRepository.fetchWholeChapter(userId, chapterId, version, lang);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
if (bookId) {
|
||||
ChapterRepo.updateLastChapterRecord(userId, bookId, chapterId, version, lang);
|
||||
}
|
||||
|
||||
return {
|
||||
chapterId: chapter.chapter_id,
|
||||
title: System.decryptDataWithUserKey(chapter.title, userKey),
|
||||
chapterOrder: chapter.chapter_order,
|
||||
chapterId: chapterContentResult.chapter_id,
|
||||
title: System.decryptDataWithUserKey(chapterContentResult.title, userEncryptionKey),
|
||||
chapterOrder: chapterContentResult.chapter_order,
|
||||
chapterContent: {
|
||||
content: chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '',
|
||||
content: chapterContentResult.content ? System.decryptDataWithUserKey(chapterContentResult.content, userEncryptionKey) : '',
|
||||
version: version,
|
||||
wordsCount: chapter.words_count
|
||||
wordsCount: chapterContentResult.words_count
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the content of a chapter for a specific version.
|
||||
* Encrypts the content before storing it in the database.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param version - The version number of the chapter content
|
||||
* @param content - The JSON content to save
|
||||
* @param wordsCount - The word count of the content
|
||||
* @param currentTime - The current timestamp (unused, actual timestamp is generated)
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the content was saved successfully, false otherwise
|
||||
*/
|
||||
public static saveChapterContent(userId: string, chapterId: string, version: number, content: JSON, wordsCount: number, currentTime: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const encryptContent: string = System.encryptDataWithUserKey(JSON.stringify(content), userKey);
|
||||
/*if (version === 2){
|
||||
const QS = new AI();
|
||||
const prompt:string = System.htmlToText(Chapter.tipTapToHtml(content));
|
||||
const response:string = await QS.request(prompt,'summary-chapter');
|
||||
console.log(response);
|
||||
}*/
|
||||
return ChapterRepo.updateChapterContent(userId, chapterId, version, encryptContent, wordsCount, System.timeStampInSeconds(), lang);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedContent: string = System.encryptDataWithUserKey(JSON.stringify(content), userEncryptionKey);
|
||||
return ChapterContentRepository.updateChapterContent(userId, chapterId, version, encryptedContent, wordsCount, System.timeStampInSeconds(), lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the last accessed chapter for a specific book.
|
||||
* Falls back to the first chapter content if no last chapter record exists.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns ChapterProps containing chapter details and content, or null if no chapters exist
|
||||
*/
|
||||
public static getLastChapter(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps | null {
|
||||
const lastChapter: LastChapterResult | null = ChapterRepo.fetchLastChapter(userId, bookId, lang);
|
||||
if (lastChapter) {
|
||||
return Chapter.getWholeChapter(userId, lastChapter.chapter_id, lastChapter.version, bookId, lang);
|
||||
const lastChapterRecord: LastChapterResult | null = ChapterRepo.fetchLastChapter(userId, bookId, lang);
|
||||
|
||||
if (lastChapterRecord) {
|
||||
return Chapter.getWholeChapter(userId, lastChapterRecord.chapter_id, lastChapterRecord.version, bookId, lang);
|
||||
}
|
||||
const chapter: ChapterContentQueryResult[] = ChapterRepo.fetchLastChapterContent(userId, bookId, lang);
|
||||
if (chapter.length === 0) {
|
||||
return null
|
||||
|
||||
const chapterContentResults: ChapterContentQueryResult[] = ChapterContentRepository.fetchLastChapterContent(userId, bookId, lang);
|
||||
|
||||
if (chapterContentResults.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const chapterData: ChapterContentQueryResult = chapter[0];
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const firstChapterContent: ChapterContentQueryResult = chapterContentResults[0];
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
return {
|
||||
chapterId: chapterData.chapter_id,
|
||||
title: chapterData.title ? System.decryptDataWithUserKey(chapterData.title, userKey) : '',
|
||||
chapterOrder: chapterData.chapter_order,
|
||||
chapterId: firstChapterContent.chapter_id,
|
||||
title: firstChapterContent.title ? System.decryptDataWithUserKey(firstChapterContent.title, userEncryptionKey) : '',
|
||||
chapterOrder: firstChapterContent.chapter_order,
|
||||
chapterContent: {
|
||||
content: chapterData.content ? System.decryptDataWithUserKey(chapterData.content, userKey) : '',
|
||||
version: chapterData.version,
|
||||
wordsCount: chapterData.words_count
|
||||
content: firstChapterContent.content ? System.decryptDataWithUserKey(firstChapterContent.content, userEncryptionKey) : '',
|
||||
version: firstChapterContent.version,
|
||||
wordsCount: firstChapterContent.words_count
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new chapter to a book.
|
||||
* Validates that the chapter name is unique within the book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param title - The title of the new chapter
|
||||
* @param wordsCount - The initial word count of the chapter
|
||||
* @param chapterOrder - The order position of the chapter
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @param existingChapterId - Optional existing chapter ID for updates
|
||||
* @returns The unique identifier of the created chapter
|
||||
* @throws Error if a chapter with the same name already exists
|
||||
*/
|
||||
public static addChapter(userId: string, bookId: string, title: string, wordsCount: number, chapterOrder: number, lang: 'fr' | 'en' = 'fr', existingChapterId?: string): string {
|
||||
const hashedTitle: string = System.hashElement(title);
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedTitle: string = System.encryptDataWithUserKey(title, userEncryptionKey);
|
||||
|
||||
if (!existingChapterId && ChapterRepo.checkNameDuplication(userId, bookId, hashedTitle, lang)) {
|
||||
throw new Error(lang === 'fr' ? `Ce nom de chapitre existe déjà.` : `This chapter name already exists.`);
|
||||
}
|
||||
|
||||
const chapterId: string = existingChapterId || System.createUniqueId();
|
||||
return ChapterRepo.insertChapter(chapterId, userId, bookId, encryptedTitle, hashedTitle, wordsCount, chapterOrder, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a chapter from the database.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter to remove
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the chapter was removed successfully, false otherwise
|
||||
*/
|
||||
public static removeChapter(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return ChapterRepo.deleteChapter(userId, chapterId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds chapter information linking a chapter to an act, plot point, and/or incident.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param actId - The act number the chapter belongs to
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param plotId - Optional plot point identifier
|
||||
* @param incidentId - Optional incident identifier
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @param existingChapterInfoId - Optional existing chapter info ID for updates
|
||||
* @returns The unique identifier of the created chapter information
|
||||
*/
|
||||
public static addChapterInformation(userId: string, chapterId: string, actId: number, bookId: string, plotId: string | null, incidentId: string | null, lang: 'fr' | 'en' = 'fr', existingChapterInfoId?: string): string {
|
||||
const chapterInfoId: string = existingChapterInfoId || System.createUniqueId();
|
||||
return ChapterRepo.insertChapterInformation(chapterInfoId, userId, chapterId, actId, bookId, plotId, incidentId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a chapter's title and order position.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param title - The new title for the chapter
|
||||
* @param chapterOrder - The new order position for the chapter
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the chapter was updated successfully, false otherwise
|
||||
*/
|
||||
public static updateChapter(userId: string, chapterId: string, title: string, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const hashedTitle: string = System.hashElement(title);
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedTitle: string = System.encryptDataWithUserKey(title, userEncryptionKey);
|
||||
return ChapterRepo.updateChapter(userId, chapterId, encryptedTitle, hashedTitle, chapterOrder, System.timeStampInSeconds(), lang);
|
||||
}
|
||||
|
||||
static updateChapterInfos(chapters: ActChapter[], userId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, lang: 'fr' | 'en' = 'fr') {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
for (const chapter of chapters) {
|
||||
const summary: string = chapter.summary ? System.encryptDataWithUserKey(chapter.summary, userKey) : '';
|
||||
const goal: string = chapter.goal ? System.encryptDataWithUserKey(chapter.goal, userKey) : '';
|
||||
const chapterId: string = chapter.chapterId;
|
||||
ChapterRepo.updateChapterInfos(userId, chapterId, actId, bookId, incidentId, plotId, summary, goal, System.timeStampInSeconds(), lang);
|
||||
/**
|
||||
* Updates chapter information for multiple chapters including summary and goal.
|
||||
* @param chapters - Array of ActChapter objects containing updated information
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param actId - The act number the chapters belong to
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param incidentId - Optional incident identifier
|
||||
* @param plotId - Optional plot point identifier
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
*/
|
||||
static updateChapterInfos(chapters: ActChapter[], userId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, lang: 'fr' | 'en' = 'fr'): void {
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
for (const chapterData of chapters) {
|
||||
const encryptedSummary: string = chapterData.summary ? System.encryptDataWithUserKey(chapterData.summary, userEncryptionKey) : '';
|
||||
const encryptedGoal: string = chapterData.goal ? System.encryptDataWithUserKey(chapterData.goal, userEncryptionKey) : '';
|
||||
const chapterId: string = chapterData.chapterId;
|
||||
ChapterRepo.updateChapterInfos(userId, chapterId, actId, bookId, incidentId, plotId, encryptedSummary, encryptedGoal, System.timeStampInSeconds(), lang);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the companion content for a chapter (previous version content).
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param version - The current version number (companion is version - 1)
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns CompanionContent containing the previous version's content
|
||||
*/
|
||||
static getCompanionContent(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): CompanionContent {
|
||||
const versionNum: number = version - 1;
|
||||
const chapterResponse: CompanionContentQueryResult[] = ChapterRepo.fetchCompanionContent(userId, chapterId, versionNum, lang);
|
||||
if (chapterResponse.length === 0) {
|
||||
const companionVersion: number = version - 1;
|
||||
const companionContentResults: CompanionContentQueryResult[] = ChapterContentRepository.fetchCompanionContent(userId, chapterId, companionVersion, lang);
|
||||
|
||||
if (companionContentResults.length === 0) {
|
||||
return {
|
||||
version: version,
|
||||
content: '',
|
||||
wordsCount: 0
|
||||
};
|
||||
}
|
||||
const chapter: CompanionContentQueryResult = chapterResponse[0];
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const companionContentData: CompanionContentQueryResult = companionContentResults[0];
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
return {
|
||||
version: chapter.version,
|
||||
content: chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '',
|
||||
wordsCount: chapter.words_count
|
||||
version: companionContentData.version,
|
||||
content: companionContentData.content ? System.decryptDataWithUserKey(companionContentData.content, userEncryptionKey) : '',
|
||||
wordsCount: companionContentData.words_count
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the story context for a chapter including act summaries, incidents, and plot points.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of ActStory containing story context organized by act
|
||||
*/
|
||||
static getChapterStory(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): ActStory[] {
|
||||
const stories: ChapterStoryQueryResult[] = ChapterRepo.fetchChapterStory(userId, chapterId, lang);
|
||||
const actStories: Record<number, ActStory> = {};
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const chapterStoryResults: ChapterStoryQueryResult[] = ChapterRepo.fetchChapterStory(userId, chapterId, lang);
|
||||
const actStoriesMap: Record<number, ActStory> = {};
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
for (const story of stories) {
|
||||
const actId: number = story.act_id;
|
||||
for (const storyResult of chapterStoryResults) {
|
||||
const actId: number = storyResult.act_id;
|
||||
|
||||
if (!actStories[actId]) {
|
||||
actStories[actId] = {
|
||||
if (!actStoriesMap[actId]) {
|
||||
actStoriesMap[actId] = {
|
||||
actId: actId,
|
||||
summary: story.summary ? System.decryptDataWithUserKey(story.summary, userKey) : '',
|
||||
chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '',
|
||||
chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : '',
|
||||
summary: storyResult.summary ? System.decryptDataWithUserKey(storyResult.summary, userEncryptionKey) : '',
|
||||
chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '',
|
||||
chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : '',
|
||||
incidents: [],
|
||||
plotPoints: []
|
||||
};
|
||||
}
|
||||
|
||||
if (story.incident_id) {
|
||||
const incidentTitle = story.incident_title ? System.decryptDataWithUserKey(story.incident_title, userKey) : '';
|
||||
const incidentSummary = story.incident_summary ? System.decryptDataWithUserKey(story.incident_summary, userKey) : '';
|
||||
if (storyResult.incident_id) {
|
||||
const decryptedIncidentTitle: string = storyResult.incident_title ? System.decryptDataWithUserKey(storyResult.incident_title, userEncryptionKey) : '';
|
||||
const decryptedIncidentSummary: string = storyResult.incident_summary ? System.decryptDataWithUserKey(storyResult.incident_summary, userEncryptionKey) : '';
|
||||
|
||||
const incidentExists = actStories[actId].incidents.some(
|
||||
(incident) => incident.incidentTitle === incidentTitle && incident.incidentSummary === incidentSummary
|
||||
const incidentAlreadyExists: boolean = actStoriesMap[actId].incidents.some(
|
||||
(existingIncident) => existingIncident.incidentTitle === decryptedIncidentTitle && existingIncident.incidentSummary === decryptedIncidentSummary
|
||||
);
|
||||
|
||||
if (!incidentExists) {
|
||||
actStories[actId].incidents.push({
|
||||
incidentTitle: incidentTitle,
|
||||
incidentSummary: incidentSummary,
|
||||
chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '',
|
||||
chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : ''
|
||||
if (!incidentAlreadyExists) {
|
||||
actStoriesMap[actId].incidents.push({
|
||||
incidentTitle: decryptedIncidentTitle,
|
||||
incidentSummary: decryptedIncidentSummary,
|
||||
chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '',
|
||||
chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (story.plot_point_id) {
|
||||
const plotTitle = story.plot_title ? System.decryptDataWithUserKey(story.plot_title, userKey) : '';
|
||||
const plotSummary = story.plot_summary ? System.decryptDataWithUserKey(story.plot_summary, userKey) : '';
|
||||
if (storyResult.plot_point_id) {
|
||||
const decryptedPlotTitle: string = storyResult.plot_title ? System.decryptDataWithUserKey(storyResult.plot_title, userEncryptionKey) : '';
|
||||
const decryptedPlotSummary: string = storyResult.plot_summary ? System.decryptDataWithUserKey(storyResult.plot_summary, userEncryptionKey) : '';
|
||||
|
||||
const plotPointExists = actStories[actId].plotPoints.some(
|
||||
(plotPoint) => plotPoint.plotTitle === plotTitle && plotPoint.plotSummary === plotSummary
|
||||
const plotPointAlreadyExists: boolean = actStoriesMap[actId].plotPoints.some(
|
||||
(existingPlotPoint) => existingPlotPoint.plotTitle === decryptedPlotTitle && existingPlotPoint.plotSummary === decryptedPlotSummary
|
||||
);
|
||||
|
||||
if (!plotPointExists) {
|
||||
actStories[actId].plotPoints.push({
|
||||
plotTitle: plotTitle,
|
||||
plotSummary: plotSummary,
|
||||
chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '',
|
||||
chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : ''
|
||||
if (!plotPointAlreadyExists) {
|
||||
actStoriesMap[actId].plotPoints.push({
|
||||
plotTitle: decryptedPlotTitle,
|
||||
plotSummary: decryptedPlotSummary,
|
||||
chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '',
|
||||
chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : ''
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(actStories);
|
||||
return Object.values(actStoriesMap);
|
||||
}
|
||||
|
||||
|
||||
static getChapterContentByVersion(userId: string, chapterid: string, version: number, lang: 'fr' | 'en' = 'fr'): string {
|
||||
const chapter: ContentQueryResult = ChapterRepo.fetchChapterContentByVersion(userId, chapterid, version, lang);
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
return chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '';
|
||||
/**
|
||||
* Retrieves the content of a specific chapter version.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param version - The version number of the content to retrieve
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The decrypted content string, or empty string if not found
|
||||
*/
|
||||
static getChapterContentByVersion(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): string {
|
||||
const contentResult: ContentQueryResult = ChapterContentRepository.fetchChapterContentByVersion(userId, chapterId, version, lang);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
return contentResult.content ? System.decryptDataWithUserKey(contentResult.content, userEncryptionKey) : '';
|
||||
}
|
||||
|
||||
static removeChapterInformation(userId: string, chapterInfoId: string, lang: 'fr' | 'en' = 'fr') {
|
||||
/**
|
||||
* Removes chapter information by its identifier.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterInfoId - The unique identifier of the chapter information to remove
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the chapter information was removed successfully, false otherwise
|
||||
*/
|
||||
static removeChapterInformation(userId: string, chapterInfoId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return ChapterRepo.deleteChapterInformation(userId, chapterInfoId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts TipTap JSON content to HTML string.
|
||||
* Handles various node types including paragraphs, headings, lists, and text marks.
|
||||
* @param tipTapContent - The TipTap JSON content to convert
|
||||
* @returns The converted HTML string
|
||||
*/
|
||||
static tipTapToHtml(tipTapContent: JSON): string {
|
||||
interface TipTapNode {
|
||||
type?: string;
|
||||
text?: string;
|
||||
content?: TipTapNode[];
|
||||
attrs?: Record<string, unknown>;
|
||||
marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;
|
||||
}
|
||||
|
||||
const escapeHtml = (text: string): string => {
|
||||
const escapeHtmlCharacters = (text: string): string => {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
@@ -317,75 +461,132 @@ export default class Chapter {
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
|
||||
const renderMarks = (text: string, marks?: Array<{ type: string; attrs?: Record<string, unknown> }>): string => {
|
||||
if (!marks || marks.length === 0) return escapeHtml(text);
|
||||
const renderTextWithMarks = (text: string, marks?: TipTapMark[]): string => {
|
||||
if (!marks || marks.length === 0) return escapeHtmlCharacters(text);
|
||||
|
||||
let result = escapeHtml(text);
|
||||
marks.forEach((mark) => {
|
||||
let renderedText: string = escapeHtmlCharacters(text);
|
||||
|
||||
marks.forEach((mark: TipTapMark) => {
|
||||
switch (mark.type) {
|
||||
case 'bold':
|
||||
result = `<strong>${result}</strong>`;
|
||||
renderedText = `<strong>${renderedText}</strong>`;
|
||||
break;
|
||||
case 'italic':
|
||||
result = `<em>${result}</em>`;
|
||||
renderedText = `<em>${renderedText}</em>`;
|
||||
break;
|
||||
case 'underline':
|
||||
result = `<u>${result}</u>`;
|
||||
renderedText = `<u>${renderedText}</u>`;
|
||||
break;
|
||||
case 'strike':
|
||||
result = `<s>${result}</s>`;
|
||||
renderedText = `<s>${renderedText}</s>`;
|
||||
break;
|
||||
case 'code':
|
||||
result = `<code>${result}</code>`;
|
||||
renderedText = `<code>${renderedText}</code>`;
|
||||
break;
|
||||
case 'link':
|
||||
const href = mark.attrs?.href || '#';
|
||||
result = `<a href="${escapeHtml(String(href))}">${result}</a>`;
|
||||
const linkHref: string = (mark.attrs?.href as string) || '#';
|
||||
renderedText = `<a href="${escapeHtmlCharacters(linkHref)}">${renderedText}</a>`;
|
||||
break;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
|
||||
return renderedText;
|
||||
};
|
||||
|
||||
const renderNode = (node: TipTapNode): string => {
|
||||
const renderTipTapNode = (node: TipTapNode): string => {
|
||||
if (!node) return '';
|
||||
|
||||
if (node.type === 'text') {
|
||||
const textContent = node.text || '\u00A0';
|
||||
return renderMarks(textContent, node.marks);
|
||||
const textContent: string = node.text || '\u00A0';
|
||||
return renderTextWithMarks(textContent, node.marks);
|
||||
}
|
||||
|
||||
const children = node.content?.map(renderNode).join('') || '';
|
||||
const textAlign = node.attrs?.textAlign ? ` style="text-align: ${node.attrs.textAlign}"` : '';
|
||||
const childrenHtml: string = node.content?.map(renderTipTapNode).join('') || '';
|
||||
const textAlignStyle: string = node.attrs?.textAlign ? ` style="text-align: ${node.attrs.textAlign}"` : '';
|
||||
|
||||
switch (node.type) {
|
||||
case 'doc':
|
||||
return children;
|
||||
return childrenHtml;
|
||||
case 'paragraph':
|
||||
return `<p${textAlign}>${children || '\u00A0'}</p>`;
|
||||
return `<p${textAlignStyle}>${childrenHtml || '\u00A0'}</p>`;
|
||||
case 'heading':
|
||||
const level = node.attrs?.level || 1;
|
||||
return `<h${level}${textAlign}>${children}</h${level}>`;
|
||||
const headingLevel: number = (node.attrs?.level as number) || 1;
|
||||
return `<h${headingLevel}${textAlignStyle}>${childrenHtml}</h${headingLevel}>`;
|
||||
case 'bulletList':
|
||||
return `<ul>${children}</ul>`;
|
||||
return `<ul>${childrenHtml}</ul>`;
|
||||
case 'orderedList':
|
||||
return `<ol>${children}</ol>`;
|
||||
return `<ol>${childrenHtml}</ol>`;
|
||||
case 'listItem':
|
||||
return `<li>${children}</li>`;
|
||||
return `<li>${childrenHtml}</li>`;
|
||||
case 'blockquote':
|
||||
return `<blockquote>${children}</blockquote>`;
|
||||
return `<blockquote>${childrenHtml}</blockquote>`;
|
||||
case 'codeBlock':
|
||||
return `<pre><code>${children}</code></pre>`;
|
||||
return `<pre><code>${childrenHtml}</code></pre>`;
|
||||
case 'hardBreak':
|
||||
return '<br />';
|
||||
case 'horizontalRule':
|
||||
return '<hr />';
|
||||
default:
|
||||
return children;
|
||||
return childrenHtml;
|
||||
}
|
||||
};
|
||||
|
||||
const contentNode = tipTapContent as unknown as TipTapNode;
|
||||
return renderNode(contentNode);
|
||||
const contentNode: TipTapNode = tipTapContent as unknown as TipTapNode;
|
||||
return renderTipTapNode(contentNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all chapters with their content data for a specific book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of ChapterContentData containing chapter details with content
|
||||
*/
|
||||
static getAllChapters(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterContentData[] {
|
||||
try {
|
||||
const completeBookData: CompleteBookData = Book.completeBookData(userId, bookId, lang);
|
||||
return Chapter.getChaptersOrSheet(completeBookData.chapters);
|
||||
} catch (error: unknown) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes book chapters to return either sheet content or chapter content.
|
||||
* If only a sheet exists (order -1), returns the sheet. Otherwise, returns all positive-order chapters.
|
||||
* @param bookChapters - Array of CompleteChapterContent from the book
|
||||
* @returns An array of ChapterContentData with processed content
|
||||
*/
|
||||
static getChaptersOrSheet(bookChapters: CompleteChapterContent[]): ChapterContentData[] {
|
||||
const processedChapters: ChapterContentData[] = [];
|
||||
const sheetContent: CompleteChapterContent | undefined = bookChapters.find(
|
||||
(chapter: CompleteChapterContent): boolean => chapter.order === -1
|
||||
);
|
||||
const regularChapter: CompleteChapterContent | undefined = bookChapters.find(
|
||||
(chapter: CompleteChapterContent): boolean => chapter.order > 0
|
||||
);
|
||||
|
||||
if (sheetContent && !regularChapter) {
|
||||
processedChapters.push({
|
||||
title: sheetContent.title,
|
||||
chapterOrder: sheetContent.order,
|
||||
content: System.htmlToText(Chapter.tipTapToHtml(JSON.parse(sheetContent.content))),
|
||||
wordsCount: 0,
|
||||
version: sheetContent.version || 0
|
||||
});
|
||||
} else if (regularChapter) {
|
||||
for (const chapterData of bookChapters) {
|
||||
if (chapterData.order < 0) continue;
|
||||
processedChapters.push({
|
||||
title: chapterData.title,
|
||||
chapterOrder: chapterData.order,
|
||||
content: System.htmlToText(Chapter.tipTapToHtml(JSON.parse(chapterData.content))),
|
||||
wordsCount: 0,
|
||||
version: chapterData.version || 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return processedChapters;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,50 +65,81 @@ export interface CharacterAttribute {
|
||||
values: Attribute[];
|
||||
}
|
||||
|
||||
export interface SyncedCharacter {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
attributes: SyncedCharacterAttribute[];
|
||||
}
|
||||
|
||||
export interface SyncedCharacterAttribute {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export default class Character {
|
||||
/**
|
||||
* Retrieves a list of all characters for a specific book.
|
||||
* Decrypts character data using the user's encryption key.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language code for localization (defaults to 'fr')
|
||||
* @returns An array of decrypted character properties
|
||||
*/
|
||||
public static getCharacterList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): CharacterProps[] {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const characters: CharacterResult[] = CharacterRepo.fetchCharacters(userId, bookId, lang);
|
||||
if (!characters) return [];
|
||||
if (characters.length === 0) return [];
|
||||
const characterList: CharacterProps[] = [];
|
||||
for (const character of characters) {
|
||||
characterList.push({
|
||||
id: character.character_id,
|
||||
name: character.first_name ? System.decryptDataWithUserKey(character.first_name, userKey) : '',
|
||||
lastName: character.last_name ? System.decryptDataWithUserKey(character.last_name, userKey) : '',
|
||||
title: character.title ? System.decryptDataWithUserKey(character.title, userKey) : '',
|
||||
category: character.category ? System.decryptDataWithUserKey(character.category, userKey) : '',
|
||||
image: character.image ? System.decryptDataWithUserKey(character.image, userKey) : '',
|
||||
role: character.role ? System.decryptDataWithUserKey(character.role, userKey) : '',
|
||||
biography: character.biography ? System.decryptDataWithUserKey(character.biography, userKey) : '',
|
||||
history: character.history ? System.decryptDataWithUserKey(character.history, userKey) : '',
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedCharacters: CharacterResult[] = CharacterRepo.fetchCharacters(userId, bookId, lang);
|
||||
if (!encryptedCharacters) return [];
|
||||
if (encryptedCharacters.length === 0) return [];
|
||||
const decryptedCharacterList: CharacterProps[] = [];
|
||||
for (const encryptedCharacter of encryptedCharacters) {
|
||||
decryptedCharacterList.push({
|
||||
id: encryptedCharacter.character_id,
|
||||
name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '',
|
||||
lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '',
|
||||
title: encryptedCharacter.title ? System.decryptDataWithUserKey(encryptedCharacter.title, userEncryptionKey) : '',
|
||||
category: encryptedCharacter.category ? System.decryptDataWithUserKey(encryptedCharacter.category, userEncryptionKey) : '',
|
||||
image: encryptedCharacter.image ? System.decryptDataWithUserKey(encryptedCharacter.image, userEncryptionKey) : '',
|
||||
role: encryptedCharacter.role ? System.decryptDataWithUserKey(encryptedCharacter.role, userEncryptionKey) : '',
|
||||
biography: encryptedCharacter.biography ? System.decryptDataWithUserKey(encryptedCharacter.biography, userEncryptionKey) : '',
|
||||
history: encryptedCharacter.history ? System.decryptDataWithUserKey(encryptedCharacter.history, userEncryptionKey) : '',
|
||||
})
|
||||
}
|
||||
return characterList;
|
||||
return decryptedCharacterList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new character with all its attributes for a specific book.
|
||||
* Encrypts all character data before storing in the database.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param character - The character data to be created
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language code for localization (defaults to 'fr')
|
||||
* @param existingCharacterId - Optional existing character ID for updates or imports
|
||||
* @returns The unique identifier of the newly created character
|
||||
*/
|
||||
public static addNewCharacter(userId: string, character: CharacterPropsPost, bookId: string, lang: 'fr' | 'en' = 'fr', existingCharacterId?: string): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
const characterId: string = existingCharacterId || System.createUniqueId();
|
||||
const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey);
|
||||
const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userKey);
|
||||
const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userKey);
|
||||
const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey);
|
||||
const encryptedImage: string = System.encryptDataWithUserKey(character.image, userKey);
|
||||
const encryptedRole: string = System.encryptDataWithUserKey(character.role, userKey);
|
||||
const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userKey);
|
||||
const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userKey);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(character.name, userEncryptionKey);
|
||||
const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userEncryptionKey);
|
||||
const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userEncryptionKey);
|
||||
const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey);
|
||||
const encryptedImage: string = System.encryptDataWithUserKey(character.image, userEncryptionKey);
|
||||
const encryptedRole: string = System.encryptDataWithUserKey(character.role, userEncryptionKey);
|
||||
const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userEncryptionKey);
|
||||
const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userEncryptionKey);
|
||||
CharacterRepo.addNewCharacter(userId, characterId, encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, bookId, lang);
|
||||
const attributes: string[] = Object.keys(character);
|
||||
for (const key of attributes) {
|
||||
if (Array.isArray(character[key as keyof CharacterPropsPost])) {
|
||||
const array = character[key as keyof CharacterPropsPost] as { name: string }[];
|
||||
if (array.length > 0) {
|
||||
for (const item of array) {
|
||||
const type: string = key;
|
||||
const name: string = item.name;
|
||||
this.addNewAttribute(characterId, userId, type, name, lang);
|
||||
const characterPropertyKeys: string[] = Object.keys(character);
|
||||
for (const propertyKey of characterPropertyKeys) {
|
||||
if (Array.isArray(character[propertyKey as keyof CharacterPropsPost])) {
|
||||
const attributeArray = character[propertyKey as keyof CharacterPropsPost] as { name: string }[];
|
||||
if (attributeArray.length > 0) {
|
||||
for (const attributeItem of attributeArray) {
|
||||
const attributeType: string = propertyKey;
|
||||
const attributeName: string = attributeItem.name;
|
||||
this.addNewAttribute(characterId, userId, attributeType, attributeName, lang);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,85 +147,128 @@ export default class Character {
|
||||
return characterId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing character's core properties.
|
||||
* Encrypts all updated data before storing in the database.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param character - The character data with updated values
|
||||
* @param lang - The language code for localization (defaults to 'fr')
|
||||
* @returns True if the update was successful, false otherwise
|
||||
*/
|
||||
static updateCharacter(userId: string, character: CharacterPropsPost, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
if (!character.id) {
|
||||
return false;
|
||||
}
|
||||
const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey);
|
||||
const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userKey);
|
||||
const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userKey);
|
||||
const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userKey);
|
||||
const encryptedImage: string = System.encryptDataWithUserKey(character.image, userKey);
|
||||
const encryptedRole: string = System.encryptDataWithUserKey(character.role, userKey);
|
||||
const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userKey);
|
||||
const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userKey);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(character.name, userEncryptionKey);
|
||||
const encryptedLastName: string = System.encryptDataWithUserKey(character.lastName, userEncryptionKey);
|
||||
const encryptedTitle: string = System.encryptDataWithUserKey(character.title, userEncryptionKey);
|
||||
const encryptedCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey);
|
||||
const encryptedImage: string = System.encryptDataWithUserKey(character.image, userEncryptionKey);
|
||||
const encryptedRole: string = System.encryptDataWithUserKey(character.role, userEncryptionKey);
|
||||
const encryptedBiography: string = System.encryptDataWithUserKey(character.biography ? character.biography : '', userEncryptionKey);
|
||||
const encryptedHistory: string = System.encryptDataWithUserKey(character.history ? character.history : '', userEncryptionKey);
|
||||
return CharacterRepo.updateCharacter(userId, character.id, encryptedName, encryptedLastName, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, System.timeStampInSeconds(), lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new attribute to a character.
|
||||
* Attributes are categorized properties like physical traits, skills, or goals.
|
||||
* @param characterId - The unique identifier of the character
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param type - The type/category of the attribute (e.g., 'physical', 'skills')
|
||||
* @param name - The value/name of the attribute
|
||||
* @param lang - The language code for localization (defaults to 'fr')
|
||||
* @param existingAttributeId - Optional existing attribute ID for updates or imports
|
||||
* @returns The unique identifier of the newly created attribute
|
||||
*/
|
||||
static addNewAttribute(characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr', existingAttributeId?: string): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
const attributeId: string = existingAttributeId || System.createUniqueId();
|
||||
const encryptedType: string = System.encryptDataWithUserKey(type, userKey);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const encryptedType: string = System.encryptDataWithUserKey(type, userEncryptionKey);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userEncryptionKey);
|
||||
return CharacterRepo.insertAttribute(attributeId, characterId, userId, encryptedType, encryptedName, lang);
|
||||
}
|
||||
|
||||
static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr') {
|
||||
/**
|
||||
* Deletes an attribute from a character.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param attributeId - The unique identifier of the attribute to delete
|
||||
* @param lang - The language code for localization (defaults to 'fr')
|
||||
* @returns True if the deletion was successful, false otherwise
|
||||
*/
|
||||
static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return CharacterRepo.deleteAttribute(userId, attributeId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all attributes for a specific character, grouped by type.
|
||||
* Decrypts attribute data using the user's encryption key.
|
||||
* @param characterId - The unique identifier of the character
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param lang - The language code for localization (defaults to 'fr')
|
||||
* @returns An array of character attributes grouped by type
|
||||
*/
|
||||
static getAttributes(characterId: string, userId: string, lang: 'fr' | 'en' = 'fr'): CharacterAttribute[] {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const attributes: AttributeResult[] = CharacterRepo.fetchAttributes(characterId, userId, lang);
|
||||
if (!attributes?.length) return [];
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedAttributes: AttributeResult[] = CharacterRepo.fetchAttributes(characterId, userId, lang);
|
||||
if (!encryptedAttributes?.length) return [];
|
||||
|
||||
const groupedMap: Map<string, Attribute[]> = new Map<string, Attribute[]>();
|
||||
const attributesByType: Map<string, Attribute[]> = new Map<string, Attribute[]>();
|
||||
|
||||
for (const attribute of attributes) {
|
||||
const type: string = System.decryptDataWithUserKey(attribute.attribute_name, userKey);
|
||||
const value: string = attribute.attribute_value ? System.decryptDataWithUserKey(attribute.attribute_value, userKey) : '';
|
||||
for (const encryptedAttribute of encryptedAttributes) {
|
||||
const decryptedType: string = System.decryptDataWithUserKey(encryptedAttribute.attribute_name, userEncryptionKey);
|
||||
const decryptedValue: string = encryptedAttribute.attribute_value ? System.decryptDataWithUserKey(encryptedAttribute.attribute_value, userEncryptionKey) : '';
|
||||
|
||||
if (!groupedMap.has(type)) {
|
||||
groupedMap.set(type, []);
|
||||
if (!attributesByType.has(decryptedType)) {
|
||||
attributesByType.set(decryptedType, []);
|
||||
}
|
||||
|
||||
groupedMap.get(type)!.push({
|
||||
id: attribute.attr_id,
|
||||
name: value
|
||||
attributesByType.get(decryptedType)!.push({
|
||||
id: encryptedAttribute.attr_id,
|
||||
name: decryptedValue
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from<[string, Attribute[]], CharacterAttribute>(
|
||||
groupedMap,
|
||||
attributesByType,
|
||||
([type, values]: [string, Attribute[]]): CharacterAttribute => ({type, values})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves complete character data including all attributes for multiple characters.
|
||||
* Used for exporting or displaying full character profiles.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param characters - An array of character IDs to retrieve
|
||||
* @param lang - The language code for localization (defaults to 'fr')
|
||||
* @returns An array of complete character objects with all their attributes
|
||||
*/
|
||||
static getCompleteCharacterList(userId: string, bookId: string, characters: string[], lang: 'fr' | 'en' = 'fr'): CompleteCharacterProps[] {
|
||||
const characterList: CompleteCharacterResult[] = CharacterRepo.fetchCompleteCharacters(userId, bookId, characters, lang);
|
||||
const encryptedCharacterList: CompleteCharacterResult[] = CharacterRepo.fetchCompleteCharacters(userId, bookId, characters, lang);
|
||||
|
||||
if (!characterList || characterList.length === 0) {
|
||||
if (!encryptedCharacterList || encryptedCharacterList.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
const completeCharactersMap = new Map<string, CompleteCharacterProps>();
|
||||
for (const character of characterList) {
|
||||
if (!character.character_id) {
|
||||
for (const encryptedCharacter of encryptedCharacterList) {
|
||||
if (!encryptedCharacter.character_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!completeCharactersMap.has(character.character_id)) {
|
||||
const personnageObj: CompleteCharacterProps = {
|
||||
if (!completeCharactersMap.has(encryptedCharacter.character_id)) {
|
||||
const decryptedCharacter: CompleteCharacterProps = {
|
||||
id: '',
|
||||
name: character.first_name ? System.decryptDataWithUserKey(character.first_name, userKey) : '',
|
||||
lastName: character.last_name ? System.decryptDataWithUserKey(character.last_name, userKey) : '',
|
||||
title: character.title ? System.decryptDataWithUserKey(character.title, userKey) : '',
|
||||
category: character.category ? System.decryptDataWithUserKey(character.category, userKey) : '',
|
||||
role: character.role ? System.decryptDataWithUserKey(character.role, userKey) : '',
|
||||
biography: character.biography ? System.decryptDataWithUserKey(character.biography, userKey) : '',
|
||||
history: character.history ? System.decryptDataWithUserKey(character.history, userKey) : '',
|
||||
name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '',
|
||||
lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '',
|
||||
title: encryptedCharacter.title ? System.decryptDataWithUserKey(encryptedCharacter.title, userEncryptionKey) : '',
|
||||
category: encryptedCharacter.category ? System.decryptDataWithUserKey(encryptedCharacter.category, userEncryptionKey) : '',
|
||||
role: encryptedCharacter.role ? System.decryptDataWithUserKey(encryptedCharacter.role, userEncryptionKey) : '',
|
||||
biography: encryptedCharacter.biography ? System.decryptDataWithUserKey(encryptedCharacter.biography, userEncryptionKey) : '',
|
||||
history: encryptedCharacter.history ? System.decryptDataWithUserKey(encryptedCharacter.history, userEncryptionKey) : '',
|
||||
physical: [],
|
||||
psychological: [],
|
||||
relations: [],
|
||||
@@ -204,36 +278,42 @@ export default class Character {
|
||||
goals: [],
|
||||
motivations: []
|
||||
};
|
||||
completeCharactersMap.set(character.character_id, personnageObj);
|
||||
completeCharactersMap.set(encryptedCharacter.character_id, decryptedCharacter);
|
||||
}
|
||||
|
||||
const personnage: CompleteCharacterProps | undefined = completeCharactersMap.get(character.character_id);
|
||||
const characterEntry: CompleteCharacterProps | undefined = completeCharactersMap.get(encryptedCharacter.character_id);
|
||||
|
||||
if (!character.attribute_name || !personnage) {
|
||||
if (!encryptedCharacter.attribute_name || !characterEntry) {
|
||||
continue;
|
||||
}
|
||||
const decryptedName: string = System.decryptDataWithUserKey(character.attribute_name, userKey);
|
||||
const decryptedValue: string = character.attribute_value ? System.decryptDataWithUserKey(character.attribute_value, userKey) : '';
|
||||
const decryptedAttributeName: string = System.decryptDataWithUserKey(encryptedCharacter.attribute_name, userEncryptionKey);
|
||||
const decryptedAttributeValue: string = encryptedCharacter.attribute_value ? System.decryptDataWithUserKey(encryptedCharacter.attribute_value, userEncryptionKey) : '';
|
||||
|
||||
if (Array.isArray(personnage[decryptedName])) {
|
||||
personnage[decryptedName].push({
|
||||
if (Array.isArray(characterEntry[decryptedAttributeName])) {
|
||||
characterEntry[decryptedAttributeName].push({
|
||||
id: '',
|
||||
name: decryptedValue
|
||||
name: decryptedAttributeValue
|
||||
});
|
||||
}
|
||||
}
|
||||
return Array.from(completeCharactersMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a formatted vCard-style string representation of characters.
|
||||
* Useful for AI context or text-based exports.
|
||||
* @param characters - An array of complete character objects to format
|
||||
* @returns A formatted string containing all character information
|
||||
*/
|
||||
static characterVCard(characters: CompleteCharacterProps[]): string {
|
||||
const charactersMap = new Map<string, CompleteCharacterProps>();
|
||||
let charactersDescription: string = '';
|
||||
const uniqueCharactersMap = new Map<string, CompleteCharacterProps>();
|
||||
let formattedCharactersDescription: string = '';
|
||||
|
||||
characters.forEach((character: CompleteCharacterProps): void => {
|
||||
const characterKey: string = character.name || character.id || 'unknown';
|
||||
const characterIdentifier: string = character.name || character.id || 'unknown';
|
||||
|
||||
if (!charactersMap.has(characterKey)) {
|
||||
charactersMap.set(characterKey, {
|
||||
if (!uniqueCharactersMap.has(characterIdentifier)) {
|
||||
uniqueCharactersMap.set(characterIdentifier, {
|
||||
name: character.name,
|
||||
lastName: character.lastName,
|
||||
category: character.category,
|
||||
@@ -244,24 +324,24 @@ export default class Character {
|
||||
});
|
||||
}
|
||||
|
||||
const characterData: CompleteCharacterProps = charactersMap.get(characterKey)!;
|
||||
const aggregatedCharacterData: CompleteCharacterProps = uniqueCharactersMap.get(characterIdentifier)!;
|
||||
|
||||
Object.keys(character).forEach((fieldName: string): void => {
|
||||
if (Array.isArray(character[fieldName])) {
|
||||
if (!characterData[fieldName]) characterData[fieldName] = [];
|
||||
(characterData[fieldName] as Attribute[]).push(...(character[fieldName] as Attribute[]));
|
||||
Object.keys(character).forEach((propertyName: string): void => {
|
||||
if (Array.isArray(character[propertyName])) {
|
||||
if (!aggregatedCharacterData[propertyName]) aggregatedCharacterData[propertyName] = [];
|
||||
(aggregatedCharacterData[propertyName] as Attribute[]).push(...(character[propertyName] as Attribute[]));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
charactersDescription = Array.from(charactersMap.values()).map((character: CompleteCharacterProps): string => {
|
||||
const descriptionFields: string[] = [];
|
||||
formattedCharactersDescription = Array.from(uniqueCharactersMap.values()).map((character: CompleteCharacterProps): string => {
|
||||
const characterDescriptionLines: string[] = [];
|
||||
const fullName: string = [character.name, character.lastName].filter(Boolean).join(' ');
|
||||
if (fullName) descriptionFields.push(`Nom : ${fullName}`);
|
||||
if (fullName) characterDescriptionLines.push(`Nom : ${fullName}`);
|
||||
|
||||
(['category', 'title', 'role', 'biography', 'history'] as const).forEach((propertyKey) => {
|
||||
if (character[propertyKey]) {
|
||||
descriptionFields.push(`${propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1)} : ${character[propertyKey]}`);
|
||||
characterDescriptionLines.push(`${propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1)} : ${character[propertyKey]}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -269,13 +349,13 @@ export default class Character {
|
||||
const propertyValue: string | Attribute[] | undefined = character[propertyKey];
|
||||
if (Array.isArray(propertyValue) && propertyValue.length > 0) {
|
||||
const capitalizedPropertyKey: string = propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1);
|
||||
const formattedValues: string = propertyValue.map((item: Attribute) => item.name).join(', ');
|
||||
descriptionFields.push(`${capitalizedPropertyKey} : ${formattedValues}`);
|
||||
const formattedAttributeValues: string = propertyValue.map((attributeItem: Attribute) => attributeItem.name).join(', ');
|
||||
characterDescriptionLines.push(`${capitalizedPropertyKey} : ${formattedAttributeValues}`);
|
||||
}
|
||||
});
|
||||
|
||||
return descriptionFields.join('\n');
|
||||
return characterDescriptionLines.join('\n');
|
||||
}).join('\n\n');
|
||||
return charactersDescription;
|
||||
return formattedCharactersDescription;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* Represents a TipTap editor node structure.
|
||||
*/
|
||||
export interface TiptapNode {
|
||||
type: string;
|
||||
content?: TiptapNode[];
|
||||
@@ -7,43 +10,74 @@ export interface TiptapNode {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class for handling TipTap content conversions.
|
||||
* Provides methods to convert TipTap JSON content to HTML and plain text.
|
||||
*/
|
||||
export default class Content {
|
||||
/**
|
||||
* Converts TipTap raw JSON string content to plain text.
|
||||
* First converts to HTML, then strips HTML tags to produce plain text.
|
||||
*
|
||||
* @param content - The TipTap JSON string to convert
|
||||
* @returns The plain text representation of the content
|
||||
*/
|
||||
static convertTipTapRawToText(content: string): string {
|
||||
const text: string = this.convertTiptapToHTMLFromString(content);
|
||||
return this.htmlToText(text);
|
||||
const htmlContent: string = this.convertTiptapToHTMLFromString(content);
|
||||
return this.htmlToText(htmlContent);
|
||||
}
|
||||
|
||||
static htmlToText(html: string) {
|
||||
/**
|
||||
* Converts HTML string to plain text by removing tags and normalizing whitespace.
|
||||
* Preserves paragraph structure by converting block elements to newlines.
|
||||
*
|
||||
* @param html - The HTML string to convert
|
||||
* @returns The plain text representation with preserved paragraph structure
|
||||
*/
|
||||
static htmlToText(html: string): string {
|
||||
return html
|
||||
.replace(/<br\s*\/?>/gi, '\n') // Gérer les <br> d'abord
|
||||
.replace(/<\/?(p|h[1-6]|div)(\s+[^>]*)?>/gi, '\n') // Balises bloc
|
||||
.replace(/<\/?[^>]+(>|$)/g, '') // Supprimer toutes les balises restantes
|
||||
.replace(/(\n\s*){2,}/g, '\n\n') // Préserver les paragraphes
|
||||
.replace(/^\s+|\s+$|(?<=\s)\s+/g, '') // Nettoyer les espaces
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<\/?(p|h[1-6]|div)(\s+[^>]*)?>/gi, '\n')
|
||||
.replace(/<\/?[^>]+(>|$)/g, '')
|
||||
.replace(/(\n\s*){2,}/g, '\n\n')
|
||||
.replace(/^\s+|\s+$|(?<=\s)\s+/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a TipTap JSON string to HTML.
|
||||
* Parses the JSON string and delegates to the node-based conversion method.
|
||||
*
|
||||
* @param jsonString - The TipTap JSON string to convert
|
||||
* @returns The HTML representation, or empty string if JSON is invalid
|
||||
*/
|
||||
static convertTiptapToHTMLFromString(jsonString: string): string {
|
||||
// Convert the JSON string to an object
|
||||
let jsonObject: TiptapNode;
|
||||
let tiptapNode: TiptapNode;
|
||||
try {
|
||||
jsonObject = JSON.parse(jsonString);
|
||||
tiptapNode = JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
console.error('Invalid JSON string:', error);
|
||||
return '';
|
||||
}
|
||||
|
||||
// Use the existing conversion function
|
||||
return this.convertTiptapToHTML(jsonObject);
|
||||
return this.convertTiptapToHTML(tiptapNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively converts a TipTap node structure to HTML.
|
||||
* Handles various node types including documents, paragraphs, headings, lists,
|
||||
* blockquotes, code blocks, and text with formatting attributes.
|
||||
*
|
||||
* @param node - The TipTap node to convert
|
||||
* @returns The HTML representation of the node and its children
|
||||
*/
|
||||
static convertTiptapToHTML(node: TiptapNode): string {
|
||||
let html = '';
|
||||
|
||||
switch (node.type) {
|
||||
case 'doc':
|
||||
if (node.content) {
|
||||
node.content.forEach(childNode => {
|
||||
node.content.forEach((childNode: TiptapNode) => {
|
||||
html += this.convertTiptapToHTML(childNode);
|
||||
});
|
||||
}
|
||||
@@ -52,7 +86,7 @@ export default class Content {
|
||||
case 'paragraph':
|
||||
html += '<p>';
|
||||
if (node.content) {
|
||||
node.content.forEach(childNode => {
|
||||
node.content.forEach((childNode: TiptapNode) => {
|
||||
html += this.convertTiptapToHTML(childNode);
|
||||
});
|
||||
}
|
||||
@@ -60,45 +94,44 @@ export default class Content {
|
||||
break;
|
||||
|
||||
case 'text':
|
||||
let textContent = node.text || '';
|
||||
let formattedText = node.text || '';
|
||||
|
||||
// Apply attributes like bold, italic, etc.
|
||||
if (node.attrs) {
|
||||
if (node.attrs.bold) {
|
||||
textContent = `<strong>${textContent}</strong>`;
|
||||
formattedText = `<strong>${formattedText}</strong>`;
|
||||
}
|
||||
if (node.attrs.italic) {
|
||||
textContent = `<em>${textContent}</em>`;
|
||||
formattedText = `<em>${formattedText}</em>`;
|
||||
}
|
||||
if (node.attrs.underline) {
|
||||
textContent = `<u>${textContent}</u>`;
|
||||
formattedText = `<u>${formattedText}</u>`;
|
||||
}
|
||||
if (node.attrs.strike) {
|
||||
textContent = `<s>${textContent}</s>`;
|
||||
formattedText = `<s>${formattedText}</s>`;
|
||||
}
|
||||
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;
|
||||
|
||||
case 'heading':
|
||||
const level = node.attrs?.level || 1;
|
||||
html += `<h${level}>`;
|
||||
const headingLevel = node.attrs?.level || 1;
|
||||
html += `<h${headingLevel}>`;
|
||||
if (node.content) {
|
||||
node.content.forEach(childNode => {
|
||||
node.content.forEach((childNode: TiptapNode) => {
|
||||
html += this.convertTiptapToHTML(childNode);
|
||||
});
|
||||
}
|
||||
html += `</h${level}>`;
|
||||
html += `</h${headingLevel}>`;
|
||||
break;
|
||||
|
||||
case 'bulletList':
|
||||
html += '<ul>';
|
||||
if (node.content) {
|
||||
node.content.forEach(childNode => {
|
||||
node.content.forEach((childNode: TiptapNode) => {
|
||||
html += this.convertTiptapToHTML(childNode);
|
||||
});
|
||||
}
|
||||
@@ -108,7 +141,7 @@ export default class Content {
|
||||
case 'orderedList':
|
||||
html += '<ol>';
|
||||
if (node.content) {
|
||||
node.content.forEach(childNode => {
|
||||
node.content.forEach((childNode: TiptapNode) => {
|
||||
html += this.convertTiptapToHTML(childNode);
|
||||
});
|
||||
}
|
||||
@@ -118,7 +151,7 @@ export default class Content {
|
||||
case 'listItem':
|
||||
html += '<li>';
|
||||
if (node.content) {
|
||||
node.content.forEach(childNode => {
|
||||
node.content.forEach((childNode: TiptapNode) => {
|
||||
html += this.convertTiptapToHTML(childNode);
|
||||
});
|
||||
}
|
||||
@@ -128,7 +161,7 @@ export default class Content {
|
||||
case 'blockquote':
|
||||
html += '<blockquote>';
|
||||
if (node.content) {
|
||||
node.content.forEach(childNode => {
|
||||
node.content.forEach((childNode: TiptapNode) => {
|
||||
html += this.convertTiptapToHTML(childNode);
|
||||
});
|
||||
}
|
||||
@@ -138,7 +171,7 @@ export default class Content {
|
||||
case 'codeBlock':
|
||||
html += '<pre><code>';
|
||||
if (node.content) {
|
||||
node.content.forEach(childNode => {
|
||||
node.content.forEach((childNode: TiptapNode) => {
|
||||
html += this.convertTiptapToHTML(childNode);
|
||||
});
|
||||
}
|
||||
@@ -148,7 +181,7 @@ export default class Content {
|
||||
default:
|
||||
console.warn(`Unhandled node type: ${node.type}`);
|
||||
if (node.content) {
|
||||
node.content.forEach(childNode => {
|
||||
node.content.forEach((childNode: TiptapNode) => {
|
||||
html += this.convertTiptapToHTML(childNode);
|
||||
});
|
||||
}
|
||||
|
||||
62
electron/database/models/Cover.ts
Normal file
62
electron/database/models/Cover.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
200
electron/database/models/Download.ts
Normal file
200
electron/database/models/Download.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,16 @@
|
||||
export const mainStyle = `h1 {
|
||||
/**
|
||||
* Default CSS styles for EPUB export formatting.
|
||||
*
|
||||
* These styles are applied to the generated EPUB content to ensure
|
||||
* consistent typography and layout across different e-readers.
|
||||
*
|
||||
* @remarks
|
||||
* - h1 elements: 24px bold font with 24px text indentation
|
||||
* - p elements: 30px text indentation, 0.7em vertical margins, justified text
|
||||
*
|
||||
* All styles use !important to override e-reader default styles.
|
||||
*/
|
||||
export const mainStyle: string = `h1 {
|
||||
font-size: 24px !important;
|
||||
font-weight: bold !important;
|
||||
text-indent: 24px !important;
|
||||
|
||||
216
electron/database/models/GuideLine.ts
Normal file
216
electron/database/models/GuideLine.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
105
electron/database/models/Incident.ts
Normal file
105
electron/database/models/Incident.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
98
electron/database/models/Issue.ts
Normal file
98
electron/database/models/Issue.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -25,22 +25,42 @@ export interface LocationProps {
|
||||
elements: Element[];
|
||||
}
|
||||
|
||||
export interface SyncedLocation {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
elements: SyncedLocationElement[];
|
||||
}
|
||||
|
||||
export interface SyncedLocationElement {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
subElements: SyncedLocationSubElement[];
|
||||
}
|
||||
|
||||
export interface SyncedLocationSubElement {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export default class Location {
|
||||
/**
|
||||
* Récupère toutes les locations pour un utilisateur et un livre donnés.
|
||||
* @param {string} userId - L'ID de l'utilisateur.
|
||||
* @param {string} bookId - L'ID du livre.
|
||||
* @returns {LocationProps[]} - Un tableau de propriétés de location.
|
||||
* @throws {Error} - Lance une erreur si une exception se produit lors de la récupération des locations.
|
||||
* Retrieves all locations for a given user and book.
|
||||
* @param userId - The user's unique identifier.
|
||||
* @param bookId - The book's unique identifier.
|
||||
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
|
||||
* @returns An array of location properties with their elements and sub-elements.
|
||||
*/
|
||||
static getAllLocations(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LocationProps[] {
|
||||
const locations: LocationQueryResult[] = LocationRepo.getLocation(userId, bookId, lang);
|
||||
if (!locations || locations.length === 0) return [];
|
||||
const locationRecords: LocationQueryResult[] = LocationRepo.getLocation(userId, bookId, lang);
|
||||
if (!locationRecords || locationRecords.length === 0) return [];
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const locationArray: LocationProps[] = [];
|
||||
|
||||
for (const record of locations) {
|
||||
for (const record of locationRecords) {
|
||||
let location = locationArray.find(loc => loc.id === record.loc_id);
|
||||
|
||||
if (!location) {
|
||||
@@ -57,12 +77,12 @@ export default class Location {
|
||||
let element = location.elements.find(elem => elem.id === record.element_id);
|
||||
if (!element) {
|
||||
const decryptedName: string = System.decryptDataWithUserKey(record.element_name, userKey);
|
||||
const decryptedDesc: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : '';
|
||||
const decryptedDescription: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : '';
|
||||
|
||||
element = {
|
||||
id: record.element_id,
|
||||
name: decryptedName,
|
||||
description: decryptedDesc,
|
||||
description: decryptedDescription,
|
||||
subElements: []
|
||||
};
|
||||
location.elements.push(element);
|
||||
@@ -73,13 +93,12 @@ export default class Location {
|
||||
|
||||
if (!subElementExists) {
|
||||
const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, userKey);
|
||||
const decryptedDesc: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : '';
|
||||
|
||||
const decryptedDescription: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : '';
|
||||
|
||||
element.subElements.push({
|
||||
id: record.sub_element_id,
|
||||
name: decryptedName,
|
||||
description: decryptedDesc
|
||||
description: decryptedDescription
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -88,47 +107,81 @@ export default class Location {
|
||||
return locationArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new location section for a book.
|
||||
* @param userId - The user's unique identifier.
|
||||
* @param locationName - The name of the location to create.
|
||||
* @param bookId - The book's unique identifier.
|
||||
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
|
||||
* @param existingLocationId - Optional existing location ID to use instead of generating a new one.
|
||||
* @returns The ID of the created location.
|
||||
*/
|
||||
static addLocationSection(userId: string, locationName: string, bookId: string, lang: 'fr' | 'en' = 'fr', existingLocationId?: string): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const originalName: string = System.hashElement(locationName);
|
||||
const hashedName: string = System.hashElement(locationName);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(locationName, userKey);
|
||||
const locationId: string = existingLocationId || System.createUniqueId();
|
||||
return LocationRepo.insertLocation(userId, locationId, bookId, encryptedName, originalName, lang);
|
||||
return LocationRepo.insertLocation(userId, locationId, bookId, encryptedName, hashedName, lang);
|
||||
}
|
||||
|
||||
static addLocationElement(userId: string, locationId: string, elementName: string, lang: 'fr' | 'en' = 'fr', existingElementId?: string) {
|
||||
/**
|
||||
* Adds a new element to a location.
|
||||
* @param userId - The user's unique identifier.
|
||||
* @param locationId - The parent location's unique identifier.
|
||||
* @param elementName - The name of the element to create.
|
||||
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
|
||||
* @param existingElementId - Optional existing element ID to use instead of generating a new one.
|
||||
* @returns The result of the insert operation.
|
||||
*/
|
||||
static addLocationElement(userId: string, locationId: string, elementName: string, lang: 'fr' | 'en' = 'fr', existingElementId?: string): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const originalName: string = System.hashElement(elementName);
|
||||
const hashedName: string = System.hashElement(elementName);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(elementName, userKey);
|
||||
const elementId: string = existingElementId || System.createUniqueId();
|
||||
return LocationRepo.insertLocationElement(userId, elementId, locationId, encryptedName, originalName, lang)
|
||||
return LocationRepo.insertLocationElement(userId, elementId, locationId, encryptedName, hashedName, lang)
|
||||
}
|
||||
|
||||
static addLocationSubElement(userId: string, elementId: string, subElementName: string, lang: 'fr' | 'en' = 'fr', existingSubElementId?: string) {
|
||||
/**
|
||||
* Adds a new sub-element to a location element.
|
||||
* @param userId - The user's unique identifier.
|
||||
* @param elementId - The parent element's unique identifier.
|
||||
* @param subElementName - The name of the sub-element to create.
|
||||
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
|
||||
* @param existingSubElementId - Optional existing sub-element ID to use instead of generating a new one.
|
||||
* @returns The result of the insert operation.
|
||||
*/
|
||||
static addLocationSubElement(userId: string, elementId: string, subElementName: string, lang: 'fr' | 'en' = 'fr', existingSubElementId?: string): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const originalName: string = System.hashElement(subElementName);
|
||||
const hashedName: string = System.hashElement(subElementName);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(subElementName, userKey);
|
||||
const subElementId: string = existingSubElementId || System.createUniqueId();
|
||||
return LocationRepo.insertLocationSubElement(userId, subElementId, elementId, encryptedName, originalName, lang)
|
||||
return LocationRepo.insertLocationSubElement(userId, subElementId, elementId, encryptedName, hashedName, lang)
|
||||
}
|
||||
|
||||
static updateLocationSection(userId: string, locations: LocationProps[], lang: 'fr' | 'en' = 'fr') {
|
||||
/**
|
||||
* Updates multiple location sections along with their elements and sub-elements.
|
||||
* @param userId - The user's unique identifier.
|
||||
* @param locations - Array of location properties to update.
|
||||
* @param lang - The language for response messages ('fr' or 'en'). Defaults to 'fr'.
|
||||
* @returns An object indicating success and a localized message.
|
||||
*/
|
||||
static updateLocationSection(userId: string, locations: LocationProps[], lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
for (const location of locations) {
|
||||
const originalName: string = System.hashElement(location.name);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(location.name, userKey);
|
||||
LocationRepo.updateLocationSection(userId, location.id, encryptedName, originalName, System.timeStampInSeconds(),lang)
|
||||
const hashedLocationName: string = System.hashElement(location.name);
|
||||
const encryptedLocationName: string = System.encryptDataWithUserKey(location.name, userKey);
|
||||
LocationRepo.updateLocationSection(userId, location.id, encryptedLocationName, hashedLocationName, System.timeStampInSeconds(), lang)
|
||||
for (const element of location.elements) {
|
||||
const originalName: string = System.hashElement(element.name);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(element.name, userKey);
|
||||
const encryptDescription: string = element.description ? System.encryptDataWithUserKey(element.description, userKey) : '';
|
||||
LocationRepo.updateLocationElement(userId, element.id, encryptedName, originalName, encryptDescription, System.timeStampInSeconds(), lang)
|
||||
const hashedElementName: string = System.hashElement(element.name);
|
||||
const encryptedElementName: string = System.encryptDataWithUserKey(element.name, userKey);
|
||||
const encryptedElementDescription: string = element.description ? System.encryptDataWithUserKey(element.description, userKey) : '';
|
||||
LocationRepo.updateLocationElement(userId, element.id, encryptedElementName, hashedElementName, encryptedElementDescription, System.timeStampInSeconds(), lang)
|
||||
for (const subElement of element.subElements) {
|
||||
const originalName: string = System.hashElement(subElement.name);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(subElement.name, userKey);
|
||||
const encryptDescription: string = subElement.description ? System.encryptDataWithUserKey(subElement.description, userKey) : '';
|
||||
LocationRepo.updateLocationSubElement(userId, subElement.id, encryptedName, originalName, encryptDescription,System.timeStampInSeconds(),lang)
|
||||
const hashedSubElementName: string = System.hashElement(subElement.name);
|
||||
const encryptedSubElementName: string = System.encryptDataWithUserKey(subElement.name, userKey);
|
||||
const encryptedSubElementDescription: string = subElement.description ? System.encryptDataWithUserKey(subElement.description, userKey) : '';
|
||||
LocationRepo.updateLocationSubElement(userId, subElement.id, encryptedSubElementName, hashedSubElementName, encryptedSubElementDescription, System.timeStampInSeconds(), lang)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,32 +191,61 @@ export default class Location {
|
||||
}
|
||||
}
|
||||
|
||||
static deleteLocationSection(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr') {
|
||||
/**
|
||||
* Deletes a location section and all its associated elements and sub-elements.
|
||||
* @param userId - The user's unique identifier.
|
||||
* @param locationId - The location's unique identifier to delete.
|
||||
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
|
||||
* @returns The result of the delete operation.
|
||||
*/
|
||||
static deleteLocationSection(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } {
|
||||
return LocationRepo.deleteLocationSection(userId, locationId, lang);
|
||||
}
|
||||
|
||||
static deleteLocationElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr') {
|
||||
/**
|
||||
* Deletes a location element and all its associated sub-elements.
|
||||
* @param userId - The user's unique identifier.
|
||||
* @param elementId - The element's unique identifier to delete.
|
||||
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
|
||||
* @returns The result of the delete operation.
|
||||
*/
|
||||
static deleteLocationElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } {
|
||||
return LocationRepo.deleteLocationElement(userId, elementId, lang);
|
||||
}
|
||||
|
||||
static deleteLocationSubElement(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr') {
|
||||
/**
|
||||
* Deletes a location sub-element.
|
||||
* @param userId - The user's unique identifier.
|
||||
* @param subElementId - The sub-element's unique identifier to delete.
|
||||
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
|
||||
* @returns The result of the delete operation.
|
||||
*/
|
||||
static deleteLocationSubElement(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } {
|
||||
return LocationRepo.deleteLocationSubElement(userId, subElementId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves location tags (elements or sub-elements) for tagging purposes.
|
||||
* Returns sub-elements when an element has multiple sub-elements, otherwise returns the element itself.
|
||||
* @param userId - The user's unique identifier.
|
||||
* @param bookId - The book's unique identifier.
|
||||
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
|
||||
* @returns An array of sub-elements suitable for tagging.
|
||||
*/
|
||||
static getLocationTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SubElement[] {
|
||||
const data: LocationElementQueryResult[] = LocationRepo.fetchLocationTags(userId, bookId, lang);
|
||||
if (!data || data.length === 0) return [];
|
||||
const tagRecords: LocationElementQueryResult[] = LocationRepo.fetchLocationTags(userId, bookId, lang);
|
||||
if (!tagRecords || tagRecords.length === 0) return [];
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const elementCounts = new Map<string, number>();
|
||||
data.forEach((record: LocationElementQueryResult): void => {
|
||||
tagRecords.forEach((record: LocationElementQueryResult): void => {
|
||||
elementCounts.set(record.element_id, (elementCounts.get(record.element_id) || 0) + 1);
|
||||
});
|
||||
|
||||
const subElements: SubElement[] = [];
|
||||
const processedIds = new Set<string>();
|
||||
|
||||
for (const record of data) {
|
||||
for (const record of tagRecords) {
|
||||
const elementCount: number = elementCounts.get(record.element_id) || 0;
|
||||
|
||||
if (elementCount > 1 && record.sub_element_id) {
|
||||
@@ -189,46 +271,58 @@ export default class Location {
|
||||
return subElements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves location elements filtered by specific tag IDs.
|
||||
* @param userId - The user's unique identifier.
|
||||
* @param locations - Array of location tag IDs to filter by.
|
||||
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
|
||||
* @returns An array of elements with their associated sub-elements.
|
||||
*/
|
||||
static getLocationsByTags(userId: string, locations: string[], lang: 'fr' | 'en' = 'fr'): Element[] {
|
||||
const locationsTags: LocationByTagResult[] = LocationRepo.fetchLocationsByTags(userId, locations, lang);
|
||||
if (!locationsTags || locationsTags.length === 0) return [];
|
||||
const locationTagRecords: LocationByTagResult[] = LocationRepo.fetchLocationsByTags(userId, locations, lang);
|
||||
if (!locationTagRecords || locationTagRecords.length === 0) return [];
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const locationTags: Element[] = [];
|
||||
for (const record of locationsTags) {
|
||||
let element: Element | undefined = locationTags.find((elem: Element): boolean => elem.name === record.element_name);
|
||||
const locationElements: Element[] = [];
|
||||
for (const record of locationTagRecords) {
|
||||
let element: Element | undefined = locationElements.find((elem: Element): boolean => elem.name === record.element_name);
|
||||
if (!element) {
|
||||
const decryptedName: string = System.decryptDataWithUserKey(record.element_name, userKey);
|
||||
const decryptedDesc: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : '';
|
||||
const decryptedDescription: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : '';
|
||||
element = {
|
||||
id: '',
|
||||
name: decryptedName,
|
||||
description: decryptedDesc,
|
||||
description: decryptedDescription,
|
||||
subElements: []
|
||||
};
|
||||
locationTags.push(element);
|
||||
locationElements.push(element);
|
||||
}
|
||||
if (record.sub_elem_name) {
|
||||
const subElementExists: boolean = element.subElements.some(sub => sub.name === record.sub_elem_name);
|
||||
if (!subElementExists) {
|
||||
const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, userKey);
|
||||
const decryptedDesc: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : '';
|
||||
const decryptedDescription: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : '';
|
||||
element.subElements.push({
|
||||
id: '',
|
||||
name: decryptedName,
|
||||
description: decryptedDesc
|
||||
description: decryptedDescription
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return locationTags;
|
||||
return locationElements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a formatted description string from an array of location elements.
|
||||
* @param locations - Array of location elements to describe.
|
||||
* @returns A formatted string with location names and descriptions.
|
||||
*/
|
||||
static locationsDescription(locations: Element[]): string {
|
||||
return locations.map((location: Element): string => {
|
||||
const fields: string[] = [];
|
||||
if (location.name) fields.push(`Nom : ${location.name}`);
|
||||
if (location.description) fields.push(`Description : ${location.description}`);
|
||||
return fields.join('\n');
|
||||
const descriptionFields: string[] = [];
|
||||
if (location.name) descriptionFields.push(`Nom : ${location.name}`);
|
||||
if (location.description) descriptionFields.push(`Description : ${location.description}`);
|
||||
return descriptionFields.join('\n');
|
||||
}).join('\n\n');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
/**
|
||||
* Supported OpenAI GPT model identifiers.
|
||||
*/
|
||||
export type GPTModel = "gpt-4o-mini" | "gpt-4o-turbo" | "gpt-3.5-turbo" | "gpt-4o" | "gpt-4.1" | "gpt-4.1-nano";
|
||||
|
||||
/**
|
||||
* Supported Anthropic Claude model identifiers.
|
||||
*/
|
||||
export type AnthropicModel =
|
||||
"claude-3-7-sonnet-20250219"
|
||||
| "claude-sonnet-4-20250514"
|
||||
@@ -7,6 +14,10 @@ export type AnthropicModel =
|
||||
| "claude-3-5-sonnet-20241022"
|
||||
| "claude-3-5-sonnet-20240620"
|
||||
| "claude-3-opus-20240229";
|
||||
|
||||
/**
|
||||
* Supported Google Gemini model identifiers.
|
||||
*/
|
||||
export type GeminiModel =
|
||||
| "gemini-2.0-flash-001"
|
||||
| "gemini-2.0-flash-lite-001"
|
||||
@@ -14,16 +25,30 @@ export type GeminiModel =
|
||||
| "gemini-2.5-flash-lite"
|
||||
| "gemini-2.5-pro";
|
||||
|
||||
/**
|
||||
* Configuration object representing an AI model with its pricing information.
|
||||
*/
|
||||
export interface AIModelConfig {
|
||||
/** Unique identifier for the AI model */
|
||||
model_id: string;
|
||||
/** Human-readable display name for the model */
|
||||
model_name: string;
|
||||
/** Brand or provider of the model (e.g., Anthropic, OpenAI, Google) */
|
||||
brand: string;
|
||||
/** Price per input tokens in USD */
|
||||
price_token_in: number;
|
||||
/** Number of input tokens per price unit */
|
||||
per_quantity_in: number;
|
||||
/** Price per output tokens in USD */
|
||||
price_token_out: number;
|
||||
/** Number of output tokens per price unit */
|
||||
per_quantity_out: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Array of all available AI models with their configurations and pricing.
|
||||
* Includes models from Anthropic (Claude), Google (Gemini), and OpenAI (GPT).
|
||||
*/
|
||||
export const AIModels: AIModelConfig[] = [
|
||||
{
|
||||
"model_id": "claude-3-5-haiku-20241022",
|
||||
@@ -250,4 +275,4 @@ export const AIModels: AIModelConfig[] = [
|
||||
"price_token_out": 0.4,
|
||||
"per_quantity_out": 1000000
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
106
electron/database/models/PlotPoint.ts
Normal file
106
electron/database/models/PlotPoint.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
876
electron/database/models/Sync.ts
Normal file
876
electron/database/models/Sync.ts
Normal 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
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
257
electron/database/models/Upload.ts
Normal file
257
electron/database/models/Upload.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,24 +3,36 @@ import System from "../System.js";
|
||||
import Book, {BookProps} from "./Book.js";
|
||||
import {getUserEncryptionKey} from "../keyManager.js";
|
||||
|
||||
interface UserAccount{
|
||||
firstName:string;
|
||||
lastName:string;
|
||||
username:string
|
||||
authorName:string;
|
||||
email:string;
|
||||
/**
|
||||
* Represents a user account with basic profile information.
|
||||
*/
|
||||
interface UserAccount {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
username: string;
|
||||
authorName: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the guide tour completion status for various features.
|
||||
*/
|
||||
export interface GuideTour {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary information for a book associated with a user.
|
||||
*/
|
||||
interface BookSummary {
|
||||
bookId: string;
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete user information response including profile data and associated books.
|
||||
*/
|
||||
export interface UserInfoResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -31,13 +43,17 @@ export interface UserInfoResponse {
|
||||
authorName: string;
|
||||
groupId: number;
|
||||
termsAccepted: boolean;
|
||||
guideTour: any[];
|
||||
guideTour: GuideTour[];
|
||||
books: BookSummary[];
|
||||
}
|
||||
|
||||
export default class User{
|
||||
/**
|
||||
* Represents a user entity with encrypted personal information storage.
|
||||
* Handles user data retrieval, creation, and updates with AES-256-CBC encryption.
|
||||
*/
|
||||
export default class User {
|
||||
|
||||
private readonly id:string;
|
||||
private readonly id: string;
|
||||
private firstName: string;
|
||||
private lastName: string;
|
||||
private username: string;
|
||||
@@ -47,7 +63,11 @@ export default class User{
|
||||
private groupId: number;
|
||||
private termsAccepted: boolean;
|
||||
|
||||
constructor(id:string){
|
||||
/**
|
||||
* Creates a new User instance with the specified identifier.
|
||||
* @param id - The unique identifier for the user
|
||||
*/
|
||||
constructor(id: string) {
|
||||
this.id = id;
|
||||
this.firstName = '';
|
||||
this.lastName = '';
|
||||
@@ -58,25 +78,35 @@ export default class User{
|
||||
this.groupId = 0;
|
||||
this.termsAccepted = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetches and decrypts the user's information from the database.
|
||||
* Populates all instance properties with the decrypted values.
|
||||
* @returns A promise that resolves when user information has been loaded
|
||||
*/
|
||||
public async getUserInfos(): Promise<void> {
|
||||
const data: UserInfosQueryResponse = UserRepo.fetchUserInfos(this.id);
|
||||
const userKey:string = getUserEncryptionKey(this.id)
|
||||
this.firstName = System.decryptDataWithUserKey(data.first_name, userKey);
|
||||
this.lastName = System.decryptDataWithUserKey(data.last_name, userKey);
|
||||
this.username = System.decryptDataWithUserKey(data.username, userKey);
|
||||
this.email = System.decryptDataWithUserKey(data.email, userKey);
|
||||
this.accountVerified = data.account_verified === 1;
|
||||
this.authorName = data.author_name ? System.decryptDataWithUserKey(data.author_name, userKey) : '';
|
||||
this.groupId = data.user_group ? data.user_group : 0;
|
||||
this.termsAccepted = data.term_accepted === 1;
|
||||
const userInfosData: UserInfosQueryResponse = UserRepo.fetchUserInfos(this.id);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(this.id);
|
||||
this.firstName = System.decryptDataWithUserKey(userInfosData.first_name, userEncryptionKey);
|
||||
this.lastName = System.decryptDataWithUserKey(userInfosData.last_name, userEncryptionKey);
|
||||
this.username = System.decryptDataWithUserKey(userInfosData.username, userEncryptionKey);
|
||||
this.email = System.decryptDataWithUserKey(userInfosData.email, userEncryptionKey);
|
||||
this.accountVerified = userInfosData.account_verified === 1;
|
||||
this.authorName = userInfosData.author_name ? System.decryptDataWithUserKey(userInfosData.author_name, userEncryptionKey) : '';
|
||||
this.groupId = userInfosData.user_group ? userInfosData.user_group : 0;
|
||||
this.termsAccepted = userInfosData.term_accepted === 1;
|
||||
}
|
||||
|
||||
public static async returnUserInfos(userId: string):Promise<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);
|
||||
await user.getUserInfos();
|
||||
const books: BookProps[] = await Book.getBooks(userId);
|
||||
const guideTour: GuideTour[] = [];
|
||||
const userBooks: BookProps[] = await Book.getBooks(userId);
|
||||
const guideTourStatus: GuideTour[] = [];
|
||||
return {
|
||||
id: user.getId(),
|
||||
name: user.getFirstName(),
|
||||
@@ -87,95 +117,194 @@ export default class User{
|
||||
authorName: user.getAuthorName(),
|
||||
groupId: user.getGroupId(),
|
||||
termsAccepted: user.isTermsAccepted(),
|
||||
guideTour: guideTour,
|
||||
books: books.map((book: BookProps):BookSummary => {
|
||||
guideTour: guideTourStatus,
|
||||
books: userBooks.map((book: BookProps): BookSummary => {
|
||||
return {
|
||||
bookId: book.id,
|
||||
title: book.title,
|
||||
subTitle: book.subTitle,
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public static async addUser(userId:string,firstName: string, lastName: string, username: string, email: string, notEncryptPassword: string, lang: 'fr' | 'en' = 'fr'): Promise<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 {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the user's first name.
|
||||
* @returns The user's first name
|
||||
*/
|
||||
public getFirstName(): string {
|
||||
return this.firstName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the user's last name.
|
||||
* @returns The user's last name
|
||||
*/
|
||||
public getLastName(): string {
|
||||
return this.lastName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the user's username.
|
||||
* @returns The user's username
|
||||
*/
|
||||
public getUsername(): string {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the user's email address.
|
||||
* @returns The user's email address
|
||||
*/
|
||||
public getEmail(): string {
|
||||
return this.email;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the user's account has been verified.
|
||||
* @returns True if the account is verified, false otherwise
|
||||
*/
|
||||
public isAccountVerified(): boolean {
|
||||
return this.accountVerified;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if the user has accepted the terms of service.
|
||||
* @returns True if the terms have been accepted, false otherwise
|
||||
*/
|
||||
public isTermsAccepted(): boolean {
|
||||
return this.termsAccepted;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the user's group identifier.
|
||||
* @returns The user's group identifier
|
||||
*/
|
||||
public getGroupId(): number {
|
||||
return this.groupId;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the user's author/pen name.
|
||||
* @returns The user's author name
|
||||
*/
|
||||
public getAuthorName(): string {
|
||||
return this.authorName;
|
||||
}
|
||||
|
||||
268
electron/database/models/World.ts
Normal file
268
electron/database/models/World.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export default class IncidentRepository {
|
||||
* @returns An array of incidents with their ID, title, and summary
|
||||
* @throws Error if the database query fails
|
||||
*/
|
||||
public static fetchAllIncidents(userId: string, bookId: string, lang: 'fr' | 'en'): IncidentQuery[] {
|
||||
public static fetchAllIncitentIncidents(userId: string, bookId: string, lang: 'fr' | 'en'): IncidentQuery[] {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'SELECT incident_id, title, summary FROM book_incidents WHERE author_id=? AND book_id=?';
|
||||
|
||||
Reference in New Issue
Block a user