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:
@@ -1,14 +1,18 @@
|
||||
import System from "../System.js";
|
||||
import { getUserEncryptionKey } from "../keyManager.js";
|
||||
import Book, { CompleteBookData } from "./Book.js";
|
||||
import ChapterRepo, {
|
||||
ActChapterQuery,
|
||||
ChapterQueryResult,
|
||||
ChapterContentQueryResult,
|
||||
LastChapterResult,
|
||||
CompanionContentQueryResult,
|
||||
ChapterStoryQueryResult,
|
||||
ContentQueryResult
|
||||
LastChapterResult
|
||||
} from "../repositories/chapter.repository.js";
|
||||
import System from "../System.js";
|
||||
import {getUserEncryptionKey} from "../keyManager.js";
|
||||
import { ActChapter, ActStory } from "./Act.js";
|
||||
import ChapterContentRepository, {
|
||||
ChapterContentQueryResult,
|
||||
CompanionContentQueryResult,
|
||||
ContentQueryResult
|
||||
} from "../repositories/chaptercontent.repository.js";
|
||||
|
||||
export interface ChapterContent {
|
||||
version: number;
|
||||
@@ -28,287 +32,427 @@ export interface ChapterProps {
|
||||
chapterContent?: ChapterContent
|
||||
}
|
||||
|
||||
export interface ActChapter {
|
||||
chapterInfoId: number;
|
||||
chapterId: string;
|
||||
title: string;
|
||||
chapterOrder: number;
|
||||
actId: number;
|
||||
incidentId: string | null;
|
||||
plotPointId: string | null;
|
||||
summary: string;
|
||||
goal: string;
|
||||
}
|
||||
|
||||
export interface CompanionContent {
|
||||
version: number;
|
||||
content: string;
|
||||
wordsCount: number;
|
||||
}
|
||||
|
||||
export interface ActStory {
|
||||
actId: number;
|
||||
summary: string;
|
||||
chapterSummary: string;
|
||||
chapterGoal: string;
|
||||
incidents: IncidentStory[];
|
||||
plotPoints: PlotPointStory[];
|
||||
export interface SyncedChapter {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
contents: SyncedChapterContent[];
|
||||
info: SyncedChapterInfo | null;
|
||||
}
|
||||
|
||||
export interface IncidentStory {
|
||||
incidentTitle: string;
|
||||
incidentSummary: string;
|
||||
chapterSummary: string;
|
||||
chapterGoal: string;
|
||||
export interface SyncedChapterContent {
|
||||
id: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export interface PlotPointStory {
|
||||
plotTitle: string;
|
||||
plotSummary: string;
|
||||
chapterSummary: string;
|
||||
chapterGoal: string;
|
||||
export interface SyncedChapterInfo {
|
||||
id: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export interface CompleteChapterContent {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
order: number;
|
||||
version?: number;
|
||||
}
|
||||
|
||||
interface TipTapNode {
|
||||
type?: string;
|
||||
text?: string;
|
||||
content?: TipTapNode[];
|
||||
attrs?: Record<string, unknown>;
|
||||
marks?: TipTapMark[];
|
||||
}
|
||||
|
||||
interface TipTapMark {
|
||||
type: string;
|
||||
attrs?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export default class Chapter {
|
||||
/**
|
||||
* Retrieves all chapters from a specific book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of ChapterProps containing chapter details
|
||||
*/
|
||||
public static getAllChaptersFromABook(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps[] {
|
||||
const chapters: ChapterQueryResult[] = ChapterRepo.fetchAllChapterFromABook(userId, bookId, lang);
|
||||
let returnChapters: ChapterProps[] = [];
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
for (const chapter of chapters) {
|
||||
const title: string = System.decryptDataWithUserKey(chapter.title, userKey);
|
||||
returnChapters.push({
|
||||
chapterId: chapter.chapter_id,
|
||||
title: title,
|
||||
chapterOrder: chapter.chapter_order
|
||||
const chapterQueryResults: ChapterQueryResult[] = ChapterRepo.fetchAllChapterFromABook(userId, bookId, lang);
|
||||
const decryptedChapters: ChapterProps[] = [];
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
for (const chapterResult of chapterQueryResults) {
|
||||
const decryptedTitle: string = System.decryptDataWithUserKey(chapterResult.title, userEncryptionKey);
|
||||
decryptedChapters.push({
|
||||
chapterId: chapterResult.chapter_id,
|
||||
title: decryptedTitle,
|
||||
chapterOrder: chapterResult.chapter_order
|
||||
});
|
||||
}
|
||||
return returnChapters;
|
||||
|
||||
return decryptedChapters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all chapters organized by acts for a specific book.
|
||||
* Caches decrypted titles to avoid redundant decryption operations.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of ActChapter containing chapter details with act information
|
||||
*/
|
||||
public static getAllChapterFromActs(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ActChapter[] {
|
||||
const query: ActChapterQuery[] = ChapterRepo.fetchAllChapterForActs(userId, bookId, lang);
|
||||
let chapters: ActChapter[] = [];
|
||||
let tempChapter: { id: string, title: string }[] = []
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
if (query.length > 0) {
|
||||
for (const chapter of query) {
|
||||
let decryptTitle: string = '';
|
||||
const newTitleId: number = tempChapter.findIndex((temp: { id: string, title: string }) => temp.id === chapter.chapter_id);
|
||||
if (newTitleId > -1) {
|
||||
decryptTitle = tempChapter[newTitleId]?.title ?? ''
|
||||
} else {
|
||||
decryptTitle = System.decryptDataWithUserKey(chapter.title, userKey);
|
||||
tempChapter.push({id: chapter.chapter_id, title: decryptTitle});
|
||||
}
|
||||
chapters.push({
|
||||
chapterId: chapter.chapter_id,
|
||||
title: decryptTitle,
|
||||
actId: chapter.act_id,
|
||||
chapterInfoId: chapter.chapter_info_id,
|
||||
chapterOrder: chapter.chapter_order,
|
||||
goal: chapter.goal ? System.decryptDataWithUserKey(chapter.goal, userKey) : '',
|
||||
summary: chapter.summary ? System.decryptDataWithUserKey(chapter.summary, userKey) : '',
|
||||
incidentId: chapter.incident_id,
|
||||
plotPointId: chapter.plot_point_id
|
||||
})
|
||||
}
|
||||
return chapters;
|
||||
} else {
|
||||
return []
|
||||
const actChapterQueryResults: ActChapterQuery[] = ChapterRepo.fetchAllChapterForActs(userId, bookId, lang);
|
||||
const actChapters: ActChapter[] = [];
|
||||
const decryptedTitleCache: { id: string; title: string }[] = [];
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
if (actChapterQueryResults.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const chapterQueryResult of actChapterQueryResults) {
|
||||
let decryptedTitle: string = '';
|
||||
const cachedTitleIndex: number = decryptedTitleCache.findIndex(
|
||||
(cachedItem: { id: string; title: string }) => cachedItem.id === chapterQueryResult.chapter_id
|
||||
);
|
||||
|
||||
if (cachedTitleIndex > -1) {
|
||||
decryptedTitle = decryptedTitleCache[cachedTitleIndex]?.title ?? '';
|
||||
} else {
|
||||
decryptedTitle = System.decryptDataWithUserKey(chapterQueryResult.title, userEncryptionKey);
|
||||
decryptedTitleCache.push({ id: chapterQueryResult.chapter_id, title: decryptedTitle });
|
||||
}
|
||||
|
||||
actChapters.push({
|
||||
chapterId: chapterQueryResult.chapter_id,
|
||||
title: decryptedTitle,
|
||||
actId: chapterQueryResult.act_id,
|
||||
chapterInfoId: chapterQueryResult.chapter_info_id,
|
||||
chapterOrder: chapterQueryResult.chapter_order,
|
||||
goal: chapterQueryResult.goal ? System.decryptDataWithUserKey(chapterQueryResult.goal, userEncryptionKey) : '',
|
||||
summary: chapterQueryResult.summary ? System.decryptDataWithUserKey(chapterQueryResult.summary, userEncryptionKey) : '',
|
||||
incidentId: chapterQueryResult.incident_id,
|
||||
plotPointId: chapterQueryResult.plot_point_id
|
||||
});
|
||||
}
|
||||
|
||||
return actChapters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a complete chapter with its content for a specific version.
|
||||
* Optionally updates the last chapter record for the book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param version - The version number of the chapter content
|
||||
* @param bookId - Optional book identifier to update last chapter record
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns ChapterProps containing chapter details and content
|
||||
*/
|
||||
public static getWholeChapter(userId: string, chapterId: string, version: number, bookId?: string, lang: 'fr' | 'en' = 'fr'): ChapterProps {
|
||||
const chapter: ChapterContentQueryResult = ChapterRepo.fetchWholeChapter(userId, chapterId, version, lang);
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const chapterContentResult: ChapterContentQueryResult = ChapterContentRepository.fetchWholeChapter(userId, chapterId, version, lang);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
if (bookId) {
|
||||
ChapterRepo.updateLastChapterRecord(userId, bookId, chapterId, version, lang);
|
||||
}
|
||||
|
||||
return {
|
||||
chapterId: chapter.chapter_id,
|
||||
title: System.decryptDataWithUserKey(chapter.title, userKey),
|
||||
chapterOrder: chapter.chapter_order,
|
||||
chapterId: chapterContentResult.chapter_id,
|
||||
title: System.decryptDataWithUserKey(chapterContentResult.title, userEncryptionKey),
|
||||
chapterOrder: chapterContentResult.chapter_order,
|
||||
chapterContent: {
|
||||
content: chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '',
|
||||
content: chapterContentResult.content ? System.decryptDataWithUserKey(chapterContentResult.content, userEncryptionKey) : '',
|
||||
version: version,
|
||||
wordsCount: chapter.words_count
|
||||
wordsCount: chapterContentResult.words_count
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the content of a chapter for a specific version.
|
||||
* Encrypts the content before storing it in the database.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param version - The version number of the chapter content
|
||||
* @param content - The JSON content to save
|
||||
* @param wordsCount - The word count of the content
|
||||
* @param currentTime - The current timestamp (unused, actual timestamp is generated)
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the content was saved successfully, false otherwise
|
||||
*/
|
||||
public static saveChapterContent(userId: string, chapterId: string, version: number, content: JSON, wordsCount: number, currentTime: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const encryptContent: string = System.encryptDataWithUserKey(JSON.stringify(content), userKey);
|
||||
/*if (version === 2){
|
||||
const QS = new AI();
|
||||
const prompt:string = System.htmlToText(Chapter.tipTapToHtml(content));
|
||||
const response:string = await QS.request(prompt,'summary-chapter');
|
||||
console.log(response);
|
||||
}*/
|
||||
return ChapterRepo.updateChapterContent(userId, chapterId, version, encryptContent, wordsCount, System.timeStampInSeconds(), lang);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedContent: string = System.encryptDataWithUserKey(JSON.stringify(content), userEncryptionKey);
|
||||
return ChapterContentRepository.updateChapterContent(userId, chapterId, version, encryptedContent, wordsCount, System.timeStampInSeconds(), lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the last accessed chapter for a specific book.
|
||||
* Falls back to the first chapter content if no last chapter record exists.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns ChapterProps containing chapter details and content, or null if no chapters exist
|
||||
*/
|
||||
public static getLastChapter(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps | null {
|
||||
const lastChapter: LastChapterResult | null = ChapterRepo.fetchLastChapter(userId, bookId, lang);
|
||||
if (lastChapter) {
|
||||
return Chapter.getWholeChapter(userId, lastChapter.chapter_id, lastChapter.version, bookId, lang);
|
||||
const lastChapterRecord: LastChapterResult | null = ChapterRepo.fetchLastChapter(userId, bookId, lang);
|
||||
|
||||
if (lastChapterRecord) {
|
||||
return Chapter.getWholeChapter(userId, lastChapterRecord.chapter_id, lastChapterRecord.version, bookId, lang);
|
||||
}
|
||||
const chapter: ChapterContentQueryResult[] = ChapterRepo.fetchLastChapterContent(userId, bookId, lang);
|
||||
if (chapter.length === 0) {
|
||||
return null
|
||||
|
||||
const chapterContentResults: ChapterContentQueryResult[] = ChapterContentRepository.fetchLastChapterContent(userId, bookId, lang);
|
||||
|
||||
if (chapterContentResults.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const chapterData: ChapterContentQueryResult = chapter[0];
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const firstChapterContent: ChapterContentQueryResult = chapterContentResults[0];
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
return {
|
||||
chapterId: chapterData.chapter_id,
|
||||
title: chapterData.title ? System.decryptDataWithUserKey(chapterData.title, userKey) : '',
|
||||
chapterOrder: chapterData.chapter_order,
|
||||
chapterId: firstChapterContent.chapter_id,
|
||||
title: firstChapterContent.title ? System.decryptDataWithUserKey(firstChapterContent.title, userEncryptionKey) : '',
|
||||
chapterOrder: firstChapterContent.chapter_order,
|
||||
chapterContent: {
|
||||
content: chapterData.content ? System.decryptDataWithUserKey(chapterData.content, userKey) : '',
|
||||
version: chapterData.version,
|
||||
wordsCount: chapterData.words_count
|
||||
content: firstChapterContent.content ? System.decryptDataWithUserKey(firstChapterContent.content, userEncryptionKey) : '',
|
||||
version: firstChapterContent.version,
|
||||
wordsCount: firstChapterContent.words_count
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new chapter to a book.
|
||||
* Validates that the chapter name is unique within the book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param title - The title of the new chapter
|
||||
* @param wordsCount - The initial word count of the chapter
|
||||
* @param chapterOrder - The order position of the chapter
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @param existingChapterId - Optional existing chapter ID for updates
|
||||
* @returns The unique identifier of the created chapter
|
||||
* @throws Error if a chapter with the same name already exists
|
||||
*/
|
||||
public static addChapter(userId: string, bookId: string, title: string, wordsCount: number, chapterOrder: number, lang: 'fr' | 'en' = 'fr', existingChapterId?: string): string {
|
||||
const hashedTitle: string = System.hashElement(title);
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedTitle: string = System.encryptDataWithUserKey(title, userEncryptionKey);
|
||||
|
||||
if (!existingChapterId && ChapterRepo.checkNameDuplication(userId, bookId, hashedTitle, lang)) {
|
||||
throw new Error(lang === 'fr' ? `Ce nom de chapitre existe déjà.` : `This chapter name already exists.`);
|
||||
}
|
||||
|
||||
const chapterId: string = existingChapterId || System.createUniqueId();
|
||||
return ChapterRepo.insertChapter(chapterId, userId, bookId, encryptedTitle, hashedTitle, wordsCount, chapterOrder, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a chapter from the database.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter to remove
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the chapter was removed successfully, false otherwise
|
||||
*/
|
||||
public static removeChapter(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return ChapterRepo.deleteChapter(userId, chapterId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds chapter information linking a chapter to an act, plot point, and/or incident.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param actId - The act number the chapter belongs to
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param plotId - Optional plot point identifier
|
||||
* @param incidentId - Optional incident identifier
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @param existingChapterInfoId - Optional existing chapter info ID for updates
|
||||
* @returns The unique identifier of the created chapter information
|
||||
*/
|
||||
public static addChapterInformation(userId: string, chapterId: string, actId: number, bookId: string, plotId: string | null, incidentId: string | null, lang: 'fr' | 'en' = 'fr', existingChapterInfoId?: string): string {
|
||||
const chapterInfoId: string = existingChapterInfoId || System.createUniqueId();
|
||||
return ChapterRepo.insertChapterInformation(chapterInfoId, userId, chapterId, actId, bookId, plotId, incidentId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a chapter's title and order position.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param title - The new title for the chapter
|
||||
* @param chapterOrder - The new order position for the chapter
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the chapter was updated successfully, false otherwise
|
||||
*/
|
||||
public static updateChapter(userId: string, chapterId: string, title: string, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const hashedTitle: string = System.hashElement(title);
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedTitle: string = System.encryptDataWithUserKey(title, userEncryptionKey);
|
||||
return ChapterRepo.updateChapter(userId, chapterId, encryptedTitle, hashedTitle, chapterOrder, System.timeStampInSeconds(), lang);
|
||||
}
|
||||
|
||||
static updateChapterInfos(chapters: ActChapter[], userId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, lang: 'fr' | 'en' = 'fr') {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
for (const chapter of chapters) {
|
||||
const summary: string = chapter.summary ? System.encryptDataWithUserKey(chapter.summary, userKey) : '';
|
||||
const goal: string = chapter.goal ? System.encryptDataWithUserKey(chapter.goal, userKey) : '';
|
||||
const chapterId: string = chapter.chapterId;
|
||||
ChapterRepo.updateChapterInfos(userId, chapterId, actId, bookId, incidentId, plotId, summary, goal, System.timeStampInSeconds(), lang);
|
||||
/**
|
||||
* Updates chapter information for multiple chapters including summary and goal.
|
||||
* @param chapters - Array of ActChapter objects containing updated information
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param actId - The act number the chapters belong to
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param incidentId - Optional incident identifier
|
||||
* @param plotId - Optional plot point identifier
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
*/
|
||||
static updateChapterInfos(chapters: ActChapter[], userId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, lang: 'fr' | 'en' = 'fr'): void {
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
for (const chapterData of chapters) {
|
||||
const encryptedSummary: string = chapterData.summary ? System.encryptDataWithUserKey(chapterData.summary, userEncryptionKey) : '';
|
||||
const encryptedGoal: string = chapterData.goal ? System.encryptDataWithUserKey(chapterData.goal, userEncryptionKey) : '';
|
||||
const chapterId: string = chapterData.chapterId;
|
||||
ChapterRepo.updateChapterInfos(userId, chapterId, actId, bookId, incidentId, plotId, encryptedSummary, encryptedGoal, System.timeStampInSeconds(), lang);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the companion content for a chapter (previous version content).
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param version - The current version number (companion is version - 1)
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns CompanionContent containing the previous version's content
|
||||
*/
|
||||
static getCompanionContent(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): CompanionContent {
|
||||
const versionNum: number = version - 1;
|
||||
const chapterResponse: CompanionContentQueryResult[] = ChapterRepo.fetchCompanionContent(userId, chapterId, versionNum, lang);
|
||||
if (chapterResponse.length === 0) {
|
||||
const companionVersion: number = version - 1;
|
||||
const companionContentResults: CompanionContentQueryResult[] = ChapterContentRepository.fetchCompanionContent(userId, chapterId, companionVersion, lang);
|
||||
|
||||
if (companionContentResults.length === 0) {
|
||||
return {
|
||||
version: version,
|
||||
content: '',
|
||||
wordsCount: 0
|
||||
};
|
||||
}
|
||||
const chapter: CompanionContentQueryResult = chapterResponse[0];
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const companionContentData: CompanionContentQueryResult = companionContentResults[0];
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
return {
|
||||
version: chapter.version,
|
||||
content: chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '',
|
||||
wordsCount: chapter.words_count
|
||||
version: companionContentData.version,
|
||||
content: companionContentData.content ? System.decryptDataWithUserKey(companionContentData.content, userEncryptionKey) : '',
|
||||
wordsCount: companionContentData.words_count
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the story context for a chapter including act summaries, incidents, and plot points.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of ActStory containing story context organized by act
|
||||
*/
|
||||
static getChapterStory(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): ActStory[] {
|
||||
const stories: ChapterStoryQueryResult[] = ChapterRepo.fetchChapterStory(userId, chapterId, lang);
|
||||
const actStories: Record<number, ActStory> = {};
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const chapterStoryResults: ChapterStoryQueryResult[] = ChapterRepo.fetchChapterStory(userId, chapterId, lang);
|
||||
const actStoriesMap: Record<number, ActStory> = {};
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
for (const story of stories) {
|
||||
const actId: number = story.act_id;
|
||||
for (const storyResult of chapterStoryResults) {
|
||||
const actId: number = storyResult.act_id;
|
||||
|
||||
if (!actStories[actId]) {
|
||||
actStories[actId] = {
|
||||
if (!actStoriesMap[actId]) {
|
||||
actStoriesMap[actId] = {
|
||||
actId: actId,
|
||||
summary: story.summary ? System.decryptDataWithUserKey(story.summary, userKey) : '',
|
||||
chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '',
|
||||
chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : '',
|
||||
summary: storyResult.summary ? System.decryptDataWithUserKey(storyResult.summary, userEncryptionKey) : '',
|
||||
chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '',
|
||||
chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : '',
|
||||
incidents: [],
|
||||
plotPoints: []
|
||||
};
|
||||
}
|
||||
|
||||
if (story.incident_id) {
|
||||
const incidentTitle = story.incident_title ? System.decryptDataWithUserKey(story.incident_title, userKey) : '';
|
||||
const incidentSummary = story.incident_summary ? System.decryptDataWithUserKey(story.incident_summary, userKey) : '';
|
||||
if (storyResult.incident_id) {
|
||||
const decryptedIncidentTitle: string = storyResult.incident_title ? System.decryptDataWithUserKey(storyResult.incident_title, userEncryptionKey) : '';
|
||||
const decryptedIncidentSummary: string = storyResult.incident_summary ? System.decryptDataWithUserKey(storyResult.incident_summary, userEncryptionKey) : '';
|
||||
|
||||
const incidentExists = actStories[actId].incidents.some(
|
||||
(incident) => incident.incidentTitle === incidentTitle && incident.incidentSummary === incidentSummary
|
||||
const incidentAlreadyExists: boolean = actStoriesMap[actId].incidents.some(
|
||||
(existingIncident) => existingIncident.incidentTitle === decryptedIncidentTitle && existingIncident.incidentSummary === decryptedIncidentSummary
|
||||
);
|
||||
|
||||
if (!incidentExists) {
|
||||
actStories[actId].incidents.push({
|
||||
incidentTitle: incidentTitle,
|
||||
incidentSummary: incidentSummary,
|
||||
chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '',
|
||||
chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : ''
|
||||
if (!incidentAlreadyExists) {
|
||||
actStoriesMap[actId].incidents.push({
|
||||
incidentTitle: decryptedIncidentTitle,
|
||||
incidentSummary: decryptedIncidentSummary,
|
||||
chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '',
|
||||
chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (story.plot_point_id) {
|
||||
const plotTitle = story.plot_title ? System.decryptDataWithUserKey(story.plot_title, userKey) : '';
|
||||
const plotSummary = story.plot_summary ? System.decryptDataWithUserKey(story.plot_summary, userKey) : '';
|
||||
if (storyResult.plot_point_id) {
|
||||
const decryptedPlotTitle: string = storyResult.plot_title ? System.decryptDataWithUserKey(storyResult.plot_title, userEncryptionKey) : '';
|
||||
const decryptedPlotSummary: string = storyResult.plot_summary ? System.decryptDataWithUserKey(storyResult.plot_summary, userEncryptionKey) : '';
|
||||
|
||||
const plotPointExists = actStories[actId].plotPoints.some(
|
||||
(plotPoint) => plotPoint.plotTitle === plotTitle && plotPoint.plotSummary === plotSummary
|
||||
const plotPointAlreadyExists: boolean = actStoriesMap[actId].plotPoints.some(
|
||||
(existingPlotPoint) => existingPlotPoint.plotTitle === decryptedPlotTitle && existingPlotPoint.plotSummary === decryptedPlotSummary
|
||||
);
|
||||
|
||||
if (!plotPointExists) {
|
||||
actStories[actId].plotPoints.push({
|
||||
plotTitle: plotTitle,
|
||||
plotSummary: plotSummary,
|
||||
chapterSummary: story.chapter_summary ? System.decryptDataWithUserKey(story.chapter_summary, userKey) : '',
|
||||
chapterGoal: story.chapter_goal ? System.decryptDataWithUserKey(story.chapter_goal, userKey) : ''
|
||||
if (!plotPointAlreadyExists) {
|
||||
actStoriesMap[actId].plotPoints.push({
|
||||
plotTitle: decryptedPlotTitle,
|
||||
plotSummary: decryptedPlotSummary,
|
||||
chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '',
|
||||
chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : ''
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(actStories);
|
||||
return Object.values(actStoriesMap);
|
||||
}
|
||||
|
||||
|
||||
static getChapterContentByVersion(userId: string, chapterid: string, version: number, lang: 'fr' | 'en' = 'fr'): string {
|
||||
const chapter: ContentQueryResult = ChapterRepo.fetchChapterContentByVersion(userId, chapterid, version, lang);
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
return chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '';
|
||||
/**
|
||||
* Retrieves the content of a specific chapter version.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterId - The unique identifier of the chapter
|
||||
* @param version - The version number of the content to retrieve
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The decrypted content string, or empty string if not found
|
||||
*/
|
||||
static getChapterContentByVersion(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): string {
|
||||
const contentResult: ContentQueryResult = ChapterContentRepository.fetchChapterContentByVersion(userId, chapterId, version, lang);
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
return contentResult.content ? System.decryptDataWithUserKey(contentResult.content, userEncryptionKey) : '';
|
||||
}
|
||||
|
||||
static removeChapterInformation(userId: string, chapterInfoId: string, lang: 'fr' | 'en' = 'fr') {
|
||||
/**
|
||||
* Removes chapter information by its identifier.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param chapterInfoId - The unique identifier of the chapter information to remove
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the chapter information was removed successfully, false otherwise
|
||||
*/
|
||||
static removeChapterInformation(userId: string, chapterInfoId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return ChapterRepo.deleteChapterInformation(userId, chapterInfoId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts TipTap JSON content to HTML string.
|
||||
* Handles various node types including paragraphs, headings, lists, and text marks.
|
||||
* @param tipTapContent - The TipTap JSON content to convert
|
||||
* @returns The converted HTML string
|
||||
*/
|
||||
static tipTapToHtml(tipTapContent: JSON): string {
|
||||
interface TipTapNode {
|
||||
type?: string;
|
||||
text?: string;
|
||||
content?: TipTapNode[];
|
||||
attrs?: Record<string, unknown>;
|
||||
marks?: Array<{ type: string; attrs?: Record<string, unknown> }>;
|
||||
}
|
||||
|
||||
const escapeHtml = (text: string): string => {
|
||||
const escapeHtmlCharacters = (text: string): string => {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
@@ -317,75 +461,132 @@ export default class Chapter {
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
|
||||
const renderMarks = (text: string, marks?: Array<{ type: string; attrs?: Record<string, unknown> }>): string => {
|
||||
if (!marks || marks.length === 0) return escapeHtml(text);
|
||||
const renderTextWithMarks = (text: string, marks?: TipTapMark[]): string => {
|
||||
if (!marks || marks.length === 0) return escapeHtmlCharacters(text);
|
||||
|
||||
let result = escapeHtml(text);
|
||||
marks.forEach((mark) => {
|
||||
let renderedText: string = escapeHtmlCharacters(text);
|
||||
|
||||
marks.forEach((mark: TipTapMark) => {
|
||||
switch (mark.type) {
|
||||
case 'bold':
|
||||
result = `<strong>${result}</strong>`;
|
||||
renderedText = `<strong>${renderedText}</strong>`;
|
||||
break;
|
||||
case 'italic':
|
||||
result = `<em>${result}</em>`;
|
||||
renderedText = `<em>${renderedText}</em>`;
|
||||
break;
|
||||
case 'underline':
|
||||
result = `<u>${result}</u>`;
|
||||
renderedText = `<u>${renderedText}</u>`;
|
||||
break;
|
||||
case 'strike':
|
||||
result = `<s>${result}</s>`;
|
||||
renderedText = `<s>${renderedText}</s>`;
|
||||
break;
|
||||
case 'code':
|
||||
result = `<code>${result}</code>`;
|
||||
renderedText = `<code>${renderedText}</code>`;
|
||||
break;
|
||||
case 'link':
|
||||
const href = mark.attrs?.href || '#';
|
||||
result = `<a href="${escapeHtml(String(href))}">${result}</a>`;
|
||||
const linkHref: string = (mark.attrs?.href as string) || '#';
|
||||
renderedText = `<a href="${escapeHtmlCharacters(linkHref)}">${renderedText}</a>`;
|
||||
break;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
|
||||
return renderedText;
|
||||
};
|
||||
|
||||
const renderNode = (node: TipTapNode): string => {
|
||||
const renderTipTapNode = (node: TipTapNode): string => {
|
||||
if (!node) return '';
|
||||
|
||||
if (node.type === 'text') {
|
||||
const textContent = node.text || '\u00A0';
|
||||
return renderMarks(textContent, node.marks);
|
||||
const textContent: string = node.text || '\u00A0';
|
||||
return renderTextWithMarks(textContent, node.marks);
|
||||
}
|
||||
|
||||
const children = node.content?.map(renderNode).join('') || '';
|
||||
const textAlign = node.attrs?.textAlign ? ` style="text-align: ${node.attrs.textAlign}"` : '';
|
||||
const childrenHtml: string = node.content?.map(renderTipTapNode).join('') || '';
|
||||
const textAlignStyle: string = node.attrs?.textAlign ? ` style="text-align: ${node.attrs.textAlign}"` : '';
|
||||
|
||||
switch (node.type) {
|
||||
case 'doc':
|
||||
return children;
|
||||
return childrenHtml;
|
||||
case 'paragraph':
|
||||
return `<p${textAlign}>${children || '\u00A0'}</p>`;
|
||||
return `<p${textAlignStyle}>${childrenHtml || '\u00A0'}</p>`;
|
||||
case 'heading':
|
||||
const level = node.attrs?.level || 1;
|
||||
return `<h${level}${textAlign}>${children}</h${level}>`;
|
||||
const headingLevel: number = (node.attrs?.level as number) || 1;
|
||||
return `<h${headingLevel}${textAlignStyle}>${childrenHtml}</h${headingLevel}>`;
|
||||
case 'bulletList':
|
||||
return `<ul>${children}</ul>`;
|
||||
return `<ul>${childrenHtml}</ul>`;
|
||||
case 'orderedList':
|
||||
return `<ol>${children}</ol>`;
|
||||
return `<ol>${childrenHtml}</ol>`;
|
||||
case 'listItem':
|
||||
return `<li>${children}</li>`;
|
||||
return `<li>${childrenHtml}</li>`;
|
||||
case 'blockquote':
|
||||
return `<blockquote>${children}</blockquote>`;
|
||||
return `<blockquote>${childrenHtml}</blockquote>`;
|
||||
case 'codeBlock':
|
||||
return `<pre><code>${children}</code></pre>`;
|
||||
return `<pre><code>${childrenHtml}</code></pre>`;
|
||||
case 'hardBreak':
|
||||
return '<br />';
|
||||
case 'horizontalRule':
|
||||
return '<hr />';
|
||||
default:
|
||||
return children;
|
||||
return childrenHtml;
|
||||
}
|
||||
};
|
||||
|
||||
const contentNode = tipTapContent as unknown as TipTapNode;
|
||||
return renderNode(contentNode);
|
||||
const contentNode: TipTapNode = tipTapContent as unknown as TipTapNode;
|
||||
return renderTipTapNode(contentNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all chapters with their content data for a specific book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of ChapterContentData containing chapter details with content
|
||||
*/
|
||||
static getAllChapters(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterContentData[] {
|
||||
try {
|
||||
const completeBookData: CompleteBookData = Book.completeBookData(userId, bookId, lang);
|
||||
return Chapter.getChaptersOrSheet(completeBookData.chapters);
|
||||
} catch (error: unknown) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes book chapters to return either sheet content or chapter content.
|
||||
* If only a sheet exists (order -1), returns the sheet. Otherwise, returns all positive-order chapters.
|
||||
* @param bookChapters - Array of CompleteChapterContent from the book
|
||||
* @returns An array of ChapterContentData with processed content
|
||||
*/
|
||||
static getChaptersOrSheet(bookChapters: CompleteChapterContent[]): ChapterContentData[] {
|
||||
const processedChapters: ChapterContentData[] = [];
|
||||
const sheetContent: CompleteChapterContent | undefined = bookChapters.find(
|
||||
(chapter: CompleteChapterContent): boolean => chapter.order === -1
|
||||
);
|
||||
const regularChapter: CompleteChapterContent | undefined = bookChapters.find(
|
||||
(chapter: CompleteChapterContent): boolean => chapter.order > 0
|
||||
);
|
||||
|
||||
if (sheetContent && !regularChapter) {
|
||||
processedChapters.push({
|
||||
title: sheetContent.title,
|
||||
chapterOrder: sheetContent.order,
|
||||
content: System.htmlToText(Chapter.tipTapToHtml(JSON.parse(sheetContent.content))),
|
||||
wordsCount: 0,
|
||||
version: sheetContent.version || 0
|
||||
});
|
||||
} else if (regularChapter) {
|
||||
for (const chapterData of bookChapters) {
|
||||
if (chapterData.order < 0) continue;
|
||||
processedChapters.push({
|
||||
title: chapterData.title,
|
||||
chapterOrder: chapterData.order,
|
||||
content: System.htmlToText(Chapter.tipTapToHtml(JSON.parse(chapterData.content))),
|
||||
wordsCount: 0,
|
||||
version: chapterData.version || 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return processedChapters;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user