import { getUserEncryptionKey } from "../keyManager.js"; import System from "../System.js"; import { BookSyncCompare, CompleteBook, SyncedBook, SyncedBookTools } from "./Book.js"; import { SyncedSpell, SyncedSpellTag } from "./Spell.js"; import SpellRepo, { BookSpellsTable, SyncedSpellResult } from "../repositories/spell.repo.js"; import SpellTagRepo, { BookSpellTagsTable, SyncedSpellTagResult } from "../repositories/spelltag.repo.js"; import BookRepo, { EritBooksTable, SyncedBookResult, BookToolsTable, SyncedBookToolsResult } 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 { 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 decryptedSpells: BookSpellsTable[] = []; const decryptedSpellTags: BookSpellTagsTable[] = []; 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; const spellIds: string[] = syncCompareData.spells; const spellTagIds: string[] = syncCompareData.spellTags; 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, nickname: characterRecord.nickname ? System.decryptDataWithUserKey(characterRecord.nickname, userEncryptionKey) : null, age: characterRecord.age ? System.decryptDataWithUserKey(characterRecord.age, userEncryptionKey) : null, gender: characterRecord.gender ? System.decryptDataWithUserKey(characterRecord.gender, userEncryptionKey) : null, species: characterRecord.species ? System.decryptDataWithUserKey(characterRecord.species, userEncryptionKey) : null, nationality: characterRecord.nationality ? System.decryptDataWithUserKey(characterRecord.nationality, userEncryptionKey) : null, status: characterRecord.status ? System.decryptDataWithUserKey(characterRecord.status, 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, speech_pattern: characterRecord.speech_pattern ? System.decryptDataWithUserKey(characterRecord.speech_pattern, userEncryptionKey) : null, catchphrase: characterRecord.catchphrase ? System.decryptDataWithUserKey(characterRecord.catchphrase, userEncryptionKey) : null, residence: characterRecord.residence ? System.decryptDataWithUserKey(characterRecord.residence, userEncryptionKey) : null, notes: characterRecord.notes ? System.decryptDataWithUserKey(characterRecord.notes, userEncryptionKey) : null, color: characterRecord.color ? System.decryptDataWithUserKey(characterRecord.color, 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) }); } } } if (syncCompareData.guideLine) { const guidelineResults: BookGuideLineTable[] = await GuidelineRepo.fetchBookGuideLineTable(userId, syncCompareData.id, lang); if (guidelineResults.length > 0) { const guidelineRecord: BookGuideLineTable = guidelineResults[0]; decryptedGuideLines.push({ ...guidelineRecord, tone: guidelineRecord.tone ? System.decryptDataWithUserKey(guidelineRecord.tone, userEncryptionKey) : null, atmosphere: guidelineRecord.atmosphere ? System.decryptDataWithUserKey(guidelineRecord.atmosphere, userEncryptionKey) : null, writing_style: guidelineRecord.writing_style ? System.decryptDataWithUserKey(guidelineRecord.writing_style, userEncryptionKey) : null, themes: guidelineRecord.themes ? System.decryptDataWithUserKey(guidelineRecord.themes, userEncryptionKey) : null, symbolism: guidelineRecord.symbolism ? System.decryptDataWithUserKey(guidelineRecord.symbolism, userEncryptionKey) : null, motifs: guidelineRecord.motifs ? System.decryptDataWithUserKey(guidelineRecord.motifs, userEncryptionKey) : null, narrative_voice: guidelineRecord.narrative_voice ? System.decryptDataWithUserKey(guidelineRecord.narrative_voice, userEncryptionKey) : null, pacing: guidelineRecord.pacing ? System.decryptDataWithUserKey(guidelineRecord.pacing, userEncryptionKey) : null, intended_audience: guidelineRecord.intended_audience ? System.decryptDataWithUserKey(guidelineRecord.intended_audience, userEncryptionKey) : null, key_messages: guidelineRecord.key_messages ? System.decryptDataWithUserKey(guidelineRecord.key_messages, userEncryptionKey) : null }); } } if (syncCompareData.aiGuideLine) { const aiGuidelineResults: BookAIGuideLineTable[] = await GuidelineRepo.fetchBookAIGuideLine(userId, syncCompareData.id, lang); if (aiGuidelineResults.length > 0) { const aiGuidelineRecord: BookAIGuideLineTable = aiGuidelineResults[0]; decryptedAIGuideLines.push({ ...aiGuidelineRecord, global_resume: aiGuidelineRecord.global_resume ? System.decryptDataWithUserKey(aiGuidelineRecord.global_resume, userEncryptionKey) : null, themes: aiGuidelineRecord.themes ? System.decryptDataWithUserKey(aiGuidelineRecord.themes, userEncryptionKey) : null, tone: aiGuidelineRecord.tone ? System.decryptDataWithUserKey(aiGuidelineRecord.tone, userEncryptionKey) : null, atmosphere: aiGuidelineRecord.atmosphere ? System.decryptDataWithUserKey(aiGuidelineRecord.atmosphere, userEncryptionKey) : null, current_resume: aiGuidelineRecord.current_resume ? System.decryptDataWithUserKey(aiGuidelineRecord.current_resume, userEncryptionKey) : null }); } } if (spellTagIds && spellTagIds.length > 0) { for (const spellTagId of spellTagIds) { const spellTagRecord: BookSpellTagsTable | null = SpellTagRepo.fetchSpellTagTableById(userId, spellTagId, lang); if (spellTagRecord) { decryptedSpellTags.push({ ...spellTagRecord, name: System.decryptDataWithUserKey(spellTagRecord.name, userEncryptionKey) }); } } } if (spellIds && spellIds.length > 0) { for (const spellId of spellIds) { const spellRecord: BookSpellsTable | null = SpellRepo.fetchSpellTableById(userId, spellId, lang); if (spellRecord) { decryptedSpells.push({ ...spellRecord, name: System.decryptDataWithUserKey(spellRecord.name, userEncryptionKey), description: System.decryptDataWithUserKey(spellRecord.description, userEncryptionKey), appearance: System.decryptDataWithUserKey(spellRecord.appearance, userEncryptionKey), tags: System.decryptDataWithUserKey(spellRecord.tags, userEncryptionKey), power_level: spellRecord.power_level ? System.decryptDataWithUserKey(spellRecord.power_level, userEncryptionKey) : null, components: spellRecord.components ? System.decryptDataWithUserKey(spellRecord.components, userEncryptionKey) : null, limitations: spellRecord.limitations ? System.decryptDataWithUserKey(spellRecord.limitations, userEncryptionKey) : null, notes: spellRecord.notes ? System.decryptDataWithUserKey(spellRecord.notes, userEncryptionKey) : null }); } } } 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 }); } const bookToolsResult: BookToolsTable | null = BookRepo.fetchBookTools(userId, syncCompareData.id, lang); const bookTools: BookToolsTable[] = bookToolsResult ? [bookToolsResult] : []; 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, bookTools: bookTools, spells: decryptedSpells, spellTags: decryptedSpellTags }; } /** * 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 { 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 serverGuideLines: BookGuideLineTable[] = completeBook.guideLine; const serverAIGuideLines: BookAIGuideLineTable[] = completeBook.aiGuideLine; 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 characterData = { firstName: System.encryptDataWithUserKey(serverCharacter.first_name, userEncryptionKey), lastName: System.encryptDataWithUserKey(serverCharacter.last_name ? serverCharacter.last_name : '', userEncryptionKey), nickname: System.encryptDataWithUserKey(serverCharacter.nickname ? serverCharacter.nickname : '', userEncryptionKey), age: System.encryptDataWithUserKey(serverCharacter.age ? serverCharacter.age : '', userEncryptionKey), gender: System.encryptDataWithUserKey(serverCharacter.gender ? serverCharacter.gender : '', userEncryptionKey), species: System.encryptDataWithUserKey(serverCharacter.species ? serverCharacter.species : '', userEncryptionKey), nationality: System.encryptDataWithUserKey(serverCharacter.nationality ? serverCharacter.nationality : '', userEncryptionKey), status: System.encryptDataWithUserKey(serverCharacter.status ? serverCharacter.status : 'alive', userEncryptionKey), category: System.encryptDataWithUserKey(serverCharacter.category, userEncryptionKey), title: System.encryptDataWithUserKey(serverCharacter.title ? serverCharacter.title : '', userEncryptionKey), image: System.encryptDataWithUserKey(serverCharacter.image ? serverCharacter.image : '', userEncryptionKey), role: System.encryptDataWithUserKey(serverCharacter.role ? serverCharacter.role : '', userEncryptionKey), biography: System.encryptDataWithUserKey(serverCharacter.biography ? serverCharacter.biography : '', userEncryptionKey), history: System.encryptDataWithUserKey(serverCharacter.history ? serverCharacter.history : '', userEncryptionKey), speechPattern: System.encryptDataWithUserKey(serverCharacter.speech_pattern ? serverCharacter.speech_pattern : '', userEncryptionKey), catchphrase: System.encryptDataWithUserKey(serverCharacter.catchphrase ? serverCharacter.catchphrase : '', userEncryptionKey), residence: System.encryptDataWithUserKey(serverCharacter.residence ? serverCharacter.residence : '', userEncryptionKey), notes: System.encryptDataWithUserKey(serverCharacter.notes ? serverCharacter.notes : '', userEncryptionKey), color: System.encryptDataWithUserKey(serverCharacter.color ? serverCharacter.color : '', userEncryptionKey) }; if (characterExists) { const updateSuccessful: boolean = CharacterRepo.updateCharacter(userId, serverCharacter.character_id, characterData, serverCharacter.last_update, lang); if (!updateSuccessful) { return false; } } else { const insertSuccessful: boolean = CharacterRepo.insertSyncCharacter(serverCharacter.character_id, bookId, userId, characterData, 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; } } } } if (serverGuideLines && serverGuideLines.length > 0) { for (const serverGuideLine of serverGuideLines) { const guideLineExists: boolean = GuidelineRepo.guideLineExist(userId, bookId, lang); const encryptedTone: string | null = serverGuideLine.tone ? System.encryptDataWithUserKey(serverGuideLine.tone, userEncryptionKey) : null; const encryptedAtmosphere: string | null = serverGuideLine.atmosphere ? System.encryptDataWithUserKey(serverGuideLine.atmosphere, userEncryptionKey) : null; const encryptedWritingStyle: string | null = serverGuideLine.writing_style ? System.encryptDataWithUserKey(serverGuideLine.writing_style, userEncryptionKey) : null; const encryptedThemes: string | null = serverGuideLine.themes ? System.encryptDataWithUserKey(serverGuideLine.themes, userEncryptionKey) : null; const encryptedSymbolism: string | null = serverGuideLine.symbolism ? System.encryptDataWithUserKey(serverGuideLine.symbolism, userEncryptionKey) : null; const encryptedMotifs: string | null = serverGuideLine.motifs ? System.encryptDataWithUserKey(serverGuideLine.motifs, userEncryptionKey) : null; const encryptedNarrativeVoice: string | null = serverGuideLine.narrative_voice ? System.encryptDataWithUserKey(serverGuideLine.narrative_voice, userEncryptionKey) : null; const encryptedPacing: string | null = serverGuideLine.pacing ? System.encryptDataWithUserKey(serverGuideLine.pacing, userEncryptionKey) : null; const encryptedKeyMessages: string | null = serverGuideLine.key_messages ? System.encryptDataWithUserKey(serverGuideLine.key_messages, userEncryptionKey) : null; const encryptedIntendedAudience: string | null = serverGuideLine.intended_audience ? System.encryptDataWithUserKey(serverGuideLine.intended_audience, userEncryptionKey) : null; if (guideLineExists) { const updateSuccessful: boolean = GuidelineRepo.updateGuideLine(userId, bookId, encryptedTone, encryptedAtmosphere, encryptedWritingStyle, encryptedThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedKeyMessages, encryptedIntendedAudience, serverGuideLine.last_update, lang); if (!updateSuccessful) { return false; } } else { const insertSuccessful: boolean = GuidelineRepo.insertSyncGuideLine(userId, bookId, encryptedTone, encryptedAtmosphere, encryptedWritingStyle, encryptedThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedIntendedAudience, encryptedKeyMessages, serverGuideLine.last_update, lang); if (!insertSuccessful) { return false; } } } } if (serverAIGuideLines && serverAIGuideLines.length > 0) { for (const serverAIGuideLine of serverAIGuideLines) { const aiGuideLineExists: boolean = GuidelineRepo.aiGuideLineExist(userId, bookId, lang); const encryptedGlobalResume: string | null = serverAIGuideLine.global_resume ? System.encryptDataWithUserKey(serverAIGuideLine.global_resume, userEncryptionKey) : null; const encryptedThemes: string | null = serverAIGuideLine.themes ? System.encryptDataWithUserKey(serverAIGuideLine.themes, userEncryptionKey) : null; const encryptedTone: string | null = serverAIGuideLine.tone ? System.encryptDataWithUserKey(serverAIGuideLine.tone, userEncryptionKey) : null; const encryptedAtmosphere: string | null = serverAIGuideLine.atmosphere ? System.encryptDataWithUserKey(serverAIGuideLine.atmosphere, userEncryptionKey) : null; const encryptedCurrentResume: string | null = serverAIGuideLine.current_resume ? System.encryptDataWithUserKey(serverAIGuideLine.current_resume, userEncryptionKey) : null; if (aiGuideLineExists) { const updateSuccessful: boolean = GuidelineRepo.insertAIGuideLine(userId, bookId, serverAIGuideLine.narrative_type, serverAIGuideLine.dialogue_type, encryptedGlobalResume, encryptedAtmosphere, serverAIGuideLine.verbe_tense, serverAIGuideLine.langue, encryptedThemes, serverAIGuideLine.last_update, lang); if (!updateSuccessful) { return false; } } else { const insertSuccessful: boolean = GuidelineRepo.insertSyncAIGuideLine(userId, bookId, encryptedGlobalResume, encryptedThemes, serverAIGuideLine.verbe_tense, serverAIGuideLine.narrative_type, serverAIGuideLine.langue, serverAIGuideLine.dialogue_type, encryptedTone, encryptedAtmosphere, encryptedCurrentResume, serverAIGuideLine.last_update, lang); if (!insertSuccessful) { return false; } } } } if (completeBook.bookTools && completeBook.bookTools.length > 0) { for (const serverBookTool of completeBook.bookTools) { const success: boolean = BookRepo.insertSyncBookTools(bookId, userId, serverBookTool.characters_enabled, serverBookTool.worlds_enabled, serverBookTool.locations_enabled, serverBookTool.spells_enabled, serverBookTool.last_update, lang); if (!success) { return false; } } } if (completeBook.spellTags && completeBook.spellTags.length > 0) { for (const serverSpellTag of completeBook.spellTags) { const spellTagExists: boolean = SpellTagRepo.isSpellTagExist(userId, serverSpellTag.tag_id, lang); const encryptedName: string = System.encryptDataWithUserKey(serverSpellTag.name, userEncryptionKey); if (spellTagExists) { const updateSuccessful: boolean = SpellTagRepo.updateSyncSpellTag(userId, serverSpellTag.tag_id, encryptedName, serverSpellTag.name_hash, serverSpellTag.color, serverSpellTag.last_update, lang); if (!updateSuccessful) { return false; } } else { const insertSuccessful: boolean = SpellTagRepo.insertSyncSpellTag(serverSpellTag.tag_id, serverSpellTag.book_id, userId, encryptedName, serverSpellTag.name_hash, serverSpellTag.color, serverSpellTag.last_update, lang); if (!insertSuccessful) { return false; } } } } if (completeBook.spells && completeBook.spells.length > 0) { for (const serverSpell of completeBook.spells) { const spellExists: boolean = SpellRepo.isSpellExist(userId, serverSpell.spell_id, lang); const encryptedName: string = System.encryptDataWithUserKey(serverSpell.name, userEncryptionKey); const encryptedDescription: string = System.encryptDataWithUserKey(serverSpell.description, userEncryptionKey); const encryptedAppearance: string = System.encryptDataWithUserKey(serverSpell.appearance, userEncryptionKey); const encryptedTags: string = System.encryptDataWithUserKey(serverSpell.tags, userEncryptionKey); const encryptedPowerLevel: string | null = serverSpell.power_level ? System.encryptDataWithUserKey(serverSpell.power_level, userEncryptionKey) : null; const encryptedComponents: string | null = serverSpell.components ? System.encryptDataWithUserKey(serverSpell.components, userEncryptionKey) : null; const encryptedLimitations: string | null = serverSpell.limitations ? System.encryptDataWithUserKey(serverSpell.limitations, userEncryptionKey) : null; const encryptedNotes: string | null = serverSpell.notes ? System.encryptDataWithUserKey(serverSpell.notes, userEncryptionKey) : null; if (spellExists) { const updateSuccessful: boolean = SpellRepo.updateSyncSpell(userId, serverSpell.spell_id, encryptedName, serverSpell.name_hash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, serverSpell.last_update, lang); if (!updateSuccessful) { return false; } } else { const insertSuccessful: boolean = SpellRepo.insertSyncSpell(serverSpell.spell_id, serverSpell.book_id, userId, encryptedName, serverSpell.name_hash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, serverSpell.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 { const userEncryptionKey: string = getUserEncryptionKey(userId); const [ allBooks, allChapters, allChapterContents, allChapterInfos, allCharacters, allCharacterAttributes, allLocations, allLocationElements, allLocationSubElements, allWorlds, allWorldElements, allIncidents, allPlotPoints, allIssues, allActSummaries, allGuidelines, allAIGuidelines, allSpells, allSpellTags ]: [ SyncedBookResult[], SyncedChapterResult[], SyncedChapterContentResult[], SyncedChapterInfoResult[], SyncedCharacterResult[], SyncedCharacterAttributeResult[], SyncedLocationResult[], SyncedLocationElementResult[], SyncedLocationSubElementResult[], SyncedWorldResult[], SyncedWorldElementResult[], SyncedIncidentResult[], SyncedPlotPointResult[], SyncedIssueResult[], SyncedActSummaryResult[], SyncedGuideLineResult[], SyncedAIGuideLineResult[], SyncedSpellResult[], SyncedSpellTagResult[] ] = 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.fetchSyncedGuideLine(userId, lang), GuidelineRepo.fetchSyncedAIGuideLine(userId, lang), SpellRepo.fetchSyncedSpells(userId, lang), SpellTagRepo.fetchSyncedSpellTags(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; const bookToolsQuery: SyncedBookToolsResult | null = BookRepo.fetchSyncedBookTools(userId, currentBookId, lang); const bookTools: SyncedBookTools | null = bookToolsQuery ? { lastUpdate: bookToolsQuery.last_update, charactersEnabled: bookToolsQuery.characters_enabled === 1, worldsEnabled: bookToolsQuery.worlds_enabled === 1, locationsEnabled: bookToolsQuery.locations_enabled === 1, spellsEnabled: bookToolsQuery.spells_enabled === 1 } : null; const bookSpells: SyncedSpell[] = allSpells .filter((spellRecord: SyncedSpellResult): boolean => spellRecord.book_id === currentBookId) .map((spellRecord: SyncedSpellResult): SyncedSpell => ({ id: spellRecord.spell_id, name: System.decryptDataWithUserKey(spellRecord.name, userEncryptionKey), lastUpdate: spellRecord.last_update })); const bookSpellTags: SyncedSpellTag[] = allSpellTags .filter((spellTagRecord: SyncedSpellTagResult): boolean => spellTagRecord.book_id === currentBookId) .map((spellTagRecord: SyncedSpellTagResult): SyncedSpellTag => ({ id: spellTagRecord.tag_id, name: System.decryptDataWithUserKey(spellTagRecord.name, userEncryptionKey), lastUpdate: spellTagRecord.last_update })); 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, bookTools: bookTools, spells: bookSpells, spellTags: bookSpellTags }; }); } }