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