import { getUserEncryptionKey } from "../keyManager.js"; import System from "../System.js"; import SeriesSyncRepo, { SyncElementType } from "../repositories/series-sync.repo.js"; import Sync from "./Sync.js"; import { CompleteSeries, SyncedSeries, SeriesTable, SeriesBooksTable, SeriesCharactersTable, SeriesCharacterAttributesTable, SeriesWorldsTable, SeriesWorldElementsTable, SeriesLocationsTable, SeriesLocationElementsTable, SeriesLocationSubElementsTable, SeriesSpellsTable, SeriesSpellTagsTable } from "./Book.js"; import SeriesRepo from "../repositories/series.repo.js"; import BookRepo from "../repositories/book.repository.js"; import SeriesCharacterRepo from "../repositories/series-character.repo.js"; import SeriesWorldRepo from "../repositories/series-world.repo.js"; import SeriesLocationRepo from "../repositories/series-location.repo.js"; import SeriesSpellRepo from "../repositories/series-spell.repo.js"; export interface SeriesSyncUploadPayload { type: SyncElementType; bookElementId: string; field: string; value: string; } export interface SeriesSyncResult { success: boolean; updatedCount: number; } export type { CompleteSeries, SyncedSeries }; /** * Handles series synchronization operations. * Manages field propagation from book elements to series elements, * and provides methods for complete series upload/download synchronization. */ export default class SeriesSync { /** * Uploads a field value from a book element to its linked series element, * then propagates the change to all other book elements linked to the same series element. * @param userId - The unique identifier of the user * @param payload - Contains type, bookElementId, field, and value * @param lang - The language for error messages ('fr' or 'en') * @returns Result containing success status and count of updated book elements */ static uploadFieldToSeries(userId: string, payload: SeriesSyncUploadPayload, lang: 'fr' | 'en'): SeriesSyncResult { const { type, bookElementId, field, value } = payload; // 1. Get the series element ID linked to the book element const seriesElementId: string | null = this.getSeriesLink(userId, type, bookElementId, lang); if (!seriesElementId) { return { success: false, updatedCount: 0 }; } // 2. Encrypt the value const userEncryptionKey: string = getUserEncryptionKey(userId); const encryptedValue: string = System.encryptDataWithUserKey(value, userEncryptionKey); // 3. Map the frontend field name to the database column name const dbColumn: string = this.mapFieldToDbColumn(type, field); // 4. Update the series element const seriesUpdated: boolean = this.updateSeriesElement(userId, type, seriesElementId, dbColumn, encryptedValue, lang); if (!seriesUpdated) { return { success: false, updatedCount: 0 }; } // 5. Map the series field to the book field (may be different for some types) const bookField: string = this.mapSeriesFieldToBookField(type, dbColumn); // 6. Update all linked book elements const updatedCount: number = this.updateLinkedBookElements(userId, type, seriesElementId, bookField, encryptedValue, lang); return { success: true, updatedCount }; } /** * Gets the series element ID linked to a book element. */ private static getSeriesLink(userId: string, type: SyncElementType, bookElementId: string, lang: 'fr' | 'en'): string | null { switch (type) { case 'character': return SeriesSyncRepo.getCharacterSeriesLink(userId, bookElementId, lang); case 'world': return SeriesSyncRepo.getWorldSeriesLink(userId, bookElementId, lang); case 'location': return SeriesSyncRepo.getLocationSeriesLink(userId, bookElementId, lang); case 'spell': return SeriesSyncRepo.getSpellSeriesLink(userId, bookElementId, lang); default: return null; } } /** * Maps frontend field names to database column names. */ private static mapFieldToDbColumn(type: SyncElementType, field: string): string { // Most fields have the same name, but some need mapping const fieldMappings: Record> = { character: { firstName: 'first_name', lastName: 'last_name', speechPattern: 'speech_pattern' }, world: {}, location: { name: 'name', locName: 'loc_name' }, spell: { powerLevel: 'power_level' } }; const typeMapping = fieldMappings[type] || {}; return typeMapping[field] || field; } /** * Updates a field in the series element. */ private static updateSeriesElement( userId: string, type: SyncElementType, seriesElementId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' ): boolean { switch (type) { case 'character': return SeriesSyncRepo.updateSeriesCharacterField(userId, seriesElementId, field, encryptedValue, lang); case 'world': return SeriesSyncRepo.updateSeriesWorldField(userId, seriesElementId, field, encryptedValue, lang); case 'location': return SeriesSyncRepo.updateSeriesLocationField(userId, seriesElementId, field, encryptedValue, lang); case 'spell': return SeriesSyncRepo.updateSeriesSpellField(userId, seriesElementId, field, encryptedValue, lang); default: return false; } } /** * Updates all book elements linked to a series element. */ private static updateLinkedBookElements( userId: string, type: SyncElementType, seriesElementId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' ): number { switch (type) { case 'character': return SeriesSyncRepo.updateLinkedBookCharactersField(userId, seriesElementId, field, encryptedValue, lang); case 'world': return SeriesSyncRepo.updateLinkedBookWorldsField(userId, seriesElementId, field, encryptedValue, lang); case 'location': return SeriesSyncRepo.updateLinkedBookLocationsField(userId, seriesElementId, field, encryptedValue, lang); case 'spell': return SeriesSyncRepo.updateLinkedBookSpellsField(userId, seriesElementId, field, encryptedValue, lang); default: return 0; } } /** * Maps series field names to book field names (they may differ). */ private static mapSeriesFieldToBookField(type: SyncElementType, seriesField: string): string { const fieldMappings: Record> = { location: { name: 'loc_name' } }; const typeMapping = fieldMappings[type] || {}; return typeMapping[seriesField] || seriesField; } // ===== SYNC METHODS ===== /** * Gets all synced series for a user. * Delegates to Sync.getSyncedSeries which already implements this functionality. */ static getSyncedSeries(userId: string, lang: 'fr' | 'en'): SyncedSeries[] { return Sync.getSyncedSeries(userId, lang); } /** * Gets a complete series with all data decrypted for upload to server. * @param userId - The unique identifier of the user * @param seriesId - The unique identifier of the series * @param lang - The language for error messages ('fr' or 'en') * @returns Complete series object with all decrypted data */ static getCompleteSeriesForUpload(userId: string, seriesId: string, lang: 'fr' | 'en'): CompleteSeries { const userEncryptionKey: string = getUserEncryptionKey(userId); // Fetch all series data - use table fetch methods that return arrays const seriesData = SeriesRepo.fetchSeriesTableForSync(userId, seriesId, lang); const seriesBooksData = SeriesRepo.fetchSeriesBooksTable(seriesId, lang); const charactersData = SeriesCharacterRepo.fetchSeriesCharactersTable(userId, seriesId, lang); const characterAttributesData = SeriesCharacterRepo.fetchSeriesCharacterAttributesBySeriesId(userId, seriesId, lang); const worldsData = SeriesWorldRepo.fetchSeriesWorldsTable(userId, seriesId, lang); const worldElementsData = SeriesWorldRepo.fetchSeriesWorldElementsBySeriesId(userId, seriesId, lang); const locationsData = SeriesLocationRepo.fetchSeriesLocationsTable(userId, seriesId, lang); const locationElementsData = SeriesLocationRepo.fetchSeriesLocationElementsBySeriesId(userId, seriesId, lang); const locationSubElementsData = SeriesLocationRepo.fetchSeriesLocationSubElementsBySeriesId(userId, seriesId, lang); const spellsData = SeriesSpellRepo.fetchSeriesSpellsTable(userId, seriesId, lang); const spellTagsData = SeriesSpellRepo.fetchSeriesSpellTagsTable(userId, seriesId, lang); // Decrypt series const series: SeriesTable[] = seriesData.map((s: SeriesTable): SeriesTable => ({ ...s, name: System.decryptDataWithUserKey(s.name, userEncryptionKey), description: s.description ? System.decryptDataWithUserKey(s.description, userEncryptionKey) : null, cover_image: s.cover_image ? System.decryptDataWithUserKey(s.cover_image, userEncryptionKey) : null })); // Decrypt characters const seriesCharacters: SeriesCharactersTable[] = charactersData.map((c): SeriesCharactersTable => { const decryptedAge: string | null = c.age ? System.decryptDataWithUserKey(c.age, userEncryptionKey) : null; return { character_id: c.character_id as string, series_id: c.series_id as string, user_id: c.user_id as string, first_name: System.decryptDataWithUserKey(c.first_name as string, userEncryptionKey), last_name: c.last_name ? System.decryptDataWithUserKey(c.last_name as string, userEncryptionKey) : null, nickname: c.nickname ? System.decryptDataWithUserKey(c.nickname as string, userEncryptionKey) : null, age: decryptedAge ? parseInt(decryptedAge, 10) : null, gender: c.gender ? System.decryptDataWithUserKey(c.gender as string, userEncryptionKey) : null, species: c.species ? System.decryptDataWithUserKey(c.species as string, userEncryptionKey) : null, nationality: c.nationality ? System.decryptDataWithUserKey(c.nationality as string, userEncryptionKey) : null, status: c.status ? System.decryptDataWithUserKey(c.status as string, userEncryptionKey) : null, title: c.title ? System.decryptDataWithUserKey(c.title as string, userEncryptionKey) : null, category: System.decryptDataWithUserKey(c.category as string, userEncryptionKey), image: c.image ? System.decryptDataWithUserKey(c.image as string, userEncryptionKey) : null, role: c.role ? System.decryptDataWithUserKey(c.role as string, userEncryptionKey) : null, biography: c.biography ? System.decryptDataWithUserKey(c.biography as string, userEncryptionKey) : null, history: c.history ? System.decryptDataWithUserKey(c.history as string, userEncryptionKey) : null, speech_pattern: c.speech_pattern ? System.decryptDataWithUserKey(c.speech_pattern as string, userEncryptionKey) : null, catchphrase: c.catchphrase ? System.decryptDataWithUserKey(c.catchphrase as string, userEncryptionKey) : null, residence: c.residence ? System.decryptDataWithUserKey(c.residence as string, userEncryptionKey) : null, notes: c.notes ? System.decryptDataWithUserKey(c.notes as string, userEncryptionKey) : null, color: c.color ? System.decryptDataWithUserKey(c.color as string, userEncryptionKey) : null, last_update: c.last_update as number }; }); // Decrypt character attributes const seriesCharacterAttributes: SeriesCharacterAttributesTable[] = characterAttributesData.map((a: SeriesCharacterAttributesTable): SeriesCharacterAttributesTable => ({ ...a, attribute_name: System.decryptDataWithUserKey(a.attribute_name, userEncryptionKey), attribute_value: System.decryptDataWithUserKey(a.attribute_value, userEncryptionKey) })); // Decrypt worlds const seriesWorlds: SeriesWorldsTable[] = worldsData.map((w: SeriesWorldsTable): SeriesWorldsTable => ({ ...w, name: System.decryptDataWithUserKey(w.name, userEncryptionKey), history: w.history ? System.decryptDataWithUserKey(w.history, userEncryptionKey) : null, politics: w.politics ? System.decryptDataWithUserKey(w.politics, userEncryptionKey) : null, economy: w.economy ? System.decryptDataWithUserKey(w.economy, userEncryptionKey) : null, religion: w.religion ? System.decryptDataWithUserKey(w.religion, userEncryptionKey) : null, languages: w.languages ? System.decryptDataWithUserKey(w.languages, userEncryptionKey) : null })); // Decrypt world elements const seriesWorldElements: SeriesWorldElementsTable[] = worldElementsData.map((e: SeriesWorldElementsTable): SeriesWorldElementsTable => ({ ...e, name: System.decryptDataWithUserKey(e.name, userEncryptionKey), description: e.description ? System.decryptDataWithUserKey(e.description, userEncryptionKey) : null })); // Decrypt locations const seriesLocations: SeriesLocationsTable[] = locationsData.map((l: SeriesLocationsTable): SeriesLocationsTable => ({ ...l, loc_name: System.decryptDataWithUserKey(l.loc_name, userEncryptionKey) })); // Decrypt location elements const seriesLocationElements: SeriesLocationElementsTable[] = locationElementsData.map((e: SeriesLocationElementsTable): SeriesLocationElementsTable => ({ ...e, element_name: System.decryptDataWithUserKey(e.element_name, userEncryptionKey), element_description: e.element_description ? System.decryptDataWithUserKey(e.element_description, userEncryptionKey) : null })); // Decrypt location sub-elements const seriesLocationSubElements: SeriesLocationSubElementsTable[] = locationSubElementsData.map((se: SeriesLocationSubElementsTable): SeriesLocationSubElementsTable => ({ ...se, sub_elem_name: System.decryptDataWithUserKey(se.sub_elem_name, userEncryptionKey), sub_elem_description: se.sub_elem_description ? System.decryptDataWithUserKey(se.sub_elem_description, userEncryptionKey) : null })); // Decrypt spells const seriesSpells: SeriesSpellsTable[] = spellsData.map((s): SeriesSpellsTable => ({ spell_id: s.spell_id as string, series_id: s.series_id as string, user_id: s.user_id as string, name: System.decryptDataWithUserKey(s.name as string, userEncryptionKey), name_hash: s.name_hash as string, description: s.description ? System.decryptDataWithUserKey(s.description as string, userEncryptionKey) : '', appearance: s.appearance ? System.decryptDataWithUserKey(s.appearance as string, userEncryptionKey) : '', tags: s.tags ? System.decryptDataWithUserKey(s.tags as string, userEncryptionKey) : '', power_level: s.power_level ? System.decryptDataWithUserKey(s.power_level as string, userEncryptionKey) : null, components: s.components ? System.decryptDataWithUserKey(s.components as string, userEncryptionKey) : null, limitations: s.limitations ? System.decryptDataWithUserKey(s.limitations as string, userEncryptionKey) : null, notes: s.notes ? System.decryptDataWithUserKey(s.notes as string, userEncryptionKey) : null, last_update: s.last_update as number })); // Decrypt spell tags const seriesSpellTags: SeriesSpellTagsTable[] = spellTagsData.map((t: SeriesSpellTagsTable): SeriesSpellTagsTable => ({ ...t, name: System.decryptDataWithUserKey(t.name, userEncryptionKey) })); return { series, seriesBooks: seriesBooksData, seriesCharacters, seriesCharacterAttributes, seriesWorlds, seriesWorldElements, seriesLocations, seriesLocationElements, seriesLocationSubElements, seriesSpells, seriesSpellTags }; } /** * Saves a complete series downloaded from the server to the local database. * Encrypts all data before storing. * @param userId - The unique identifier of the user * @param completeSeries - The complete series data from the server * @param lang - The language for error messages ('fr' or 'en') * @returns True if save was successful, false otherwise */ static saveCompleteSeries(userId: string, completeSeries: CompleteSeries, lang: 'fr' | 'en'): boolean { const userEncryptionKey: string = getUserEncryptionKey(userId); // Save series for (const series of completeSeries.series) { const encryptedName: string = System.encryptDataWithUserKey(series.name, userEncryptionKey); const encryptedDescription: string | null = series.description ? System.encryptDataWithUserKey(series.description, userEncryptionKey) : null; const encryptedCoverImage: string | null = series.cover_image ? System.encryptDataWithUserKey(series.cover_image, userEncryptionKey) : null; const success: boolean = SeriesRepo.insertSyncSeries( series.series_id, userId, encryptedName, series.hashed_name, encryptedDescription, encryptedCoverImage, series.last_update, lang ); if (!success) return false; } // Save series books (only if the book exists locally) for (const seriesBook of completeSeries.seriesBooks) { const bookExists: boolean = BookRepo.isBookExist(userId, seriesBook.book_id, lang); if (!bookExists) continue; const success: boolean = SeriesRepo.insertSyncSeriesBook( seriesBook.series_id, seriesBook.book_id, seriesBook.book_order, seriesBook.last_update, lang ); if (!success) return false; } // Save characters for (const character of completeSeries.seriesCharacters) { const encFirstName: string = System.encryptDataWithUserKey(character.first_name, userEncryptionKey); const encLastName: string | null = character.last_name ? System.encryptDataWithUserKey(character.last_name, userEncryptionKey) : null; const encNickname: string | null = character.nickname ? System.encryptDataWithUserKey(character.nickname, userEncryptionKey) : null; const encAge: string | null = character.age !== null ? System.encryptDataWithUserKey(String(character.age), userEncryptionKey) : null; const encGender: string | null = character.gender ? System.encryptDataWithUserKey(character.gender, userEncryptionKey) : null; const encSpecies: string | null = character.species ? System.encryptDataWithUserKey(character.species, userEncryptionKey) : null; const encNationality: string | null = character.nationality ? System.encryptDataWithUserKey(character.nationality, userEncryptionKey) : null; const encStatus: string | null = character.status ? System.encryptDataWithUserKey(character.status, userEncryptionKey) : null; const encTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userEncryptionKey) : null; const encCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey); const encImage: string | null = character.image ? System.encryptDataWithUserKey(character.image, userEncryptionKey) : null; const encRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userEncryptionKey) : null; const encBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userEncryptionKey) : null; const encHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userEncryptionKey) : null; const encSpeechPattern: string | null = character.speech_pattern ? System.encryptDataWithUserKey(character.speech_pattern, userEncryptionKey) : null; const encCatchphrase: string | null = character.catchphrase ? System.encryptDataWithUserKey(character.catchphrase, userEncryptionKey) : null; const encResidence: string | null = character.residence ? System.encryptDataWithUserKey(character.residence, userEncryptionKey) : null; const encNotes: string | null = character.notes ? System.encryptDataWithUserKey(character.notes, userEncryptionKey) : null; const encColor: string | null = character.color ? System.encryptDataWithUserKey(character.color, userEncryptionKey) : null; const success: boolean = SeriesCharacterRepo.insertSyncSeriesCharacter( character.character_id, character.series_id, userId, encFirstName, encLastName, encNickname, encAge, encGender, encSpecies, encNationality, encStatus, encCategory, encTitle, encImage, encRole, encBiography, encHistory, encSpeechPattern, encCatchphrase, encResidence, encNotes, encColor, character.last_update, lang ); if (!success) return false; } // Save character attributes for (const attr of completeSeries.seriesCharacterAttributes) { const encryptedName: string = System.encryptDataWithUserKey(attr.attribute_name, userEncryptionKey); const encryptedValue: string = System.encryptDataWithUserKey(attr.attribute_value, userEncryptionKey); const success: boolean = SeriesCharacterRepo.insertSyncSeriesCharacterAttribute( attr.attr_id, attr.character_id, userId, encryptedName, encryptedValue, attr.last_update, lang ); if (!success) return false; } // Save worlds for (const world of completeSeries.seriesWorlds) { const encryptedName: string = System.encryptDataWithUserKey(world.name, userEncryptionKey); const encryptedHistory: string | null = world.history ? System.encryptDataWithUserKey(world.history, userEncryptionKey) : null; const encryptedPolitics: string | null = world.politics ? System.encryptDataWithUserKey(world.politics, userEncryptionKey) : null; const encryptedEconomy: string | null = world.economy ? System.encryptDataWithUserKey(world.economy, userEncryptionKey) : null; const encryptedReligion: string | null = world.religion ? System.encryptDataWithUserKey(world.religion, userEncryptionKey) : null; const encryptedLanguages: string | null = world.languages ? System.encryptDataWithUserKey(world.languages, userEncryptionKey) : null; const success: boolean = SeriesWorldRepo.insertSyncSeriesWorld( world.world_id, world.series_id, userId, encryptedName, world.hashed_name, encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, world.last_update, lang ); if (!success) return false; } // Save world elements for (const element of completeSeries.seriesWorldElements) { const encryptedName: string = System.encryptDataWithUserKey(element.name, userEncryptionKey); const encryptedDescription: string | null = element.description ? System.encryptDataWithUserKey(element.description, userEncryptionKey) : null; const success: boolean = SeriesWorldRepo.insertSyncSeriesWorldElement( element.element_id, element.world_id, userId, element.element_type, encryptedName, element.original_name, encryptedDescription, element.last_update, lang ); if (!success) return false; } // Save locations for (const location of completeSeries.seriesLocations) { const encryptedName: string = System.encryptDataWithUserKey(location.loc_name, userEncryptionKey); const success: boolean = SeriesLocationRepo.insertSyncSeriesLocation( location.loc_id, location.series_id, userId, encryptedName, location.loc_original_name, location.last_update, lang ); if (!success) return false; } // Save location elements for (const element of completeSeries.seriesLocationElements) { const encryptedName: string = System.encryptDataWithUserKey(element.element_name, userEncryptionKey); const encryptedDescription: string | null = element.element_description ? System.encryptDataWithUserKey(element.element_description, userEncryptionKey) : null; const success: boolean = SeriesLocationRepo.insertSyncSeriesLocationElement( element.element_id, element.location_id, userId, encryptedName, element.original_name, encryptedDescription, element.last_update, lang ); if (!success) return false; } // Save location sub-elements for (const subElement of completeSeries.seriesLocationSubElements) { const encryptedName: string = System.encryptDataWithUserKey(subElement.sub_elem_name, userEncryptionKey); const encryptedDescription: string | null = subElement.sub_elem_description ? System.encryptDataWithUserKey(subElement.sub_elem_description, userEncryptionKey) : null; const success: boolean = SeriesLocationRepo.insertSyncSeriesLocationSubElement( subElement.sub_element_id, subElement.element_id, userId, encryptedName, subElement.original_name, encryptedDescription, subElement.last_update, lang ); if (!success) return false; } // Save spells for (const spell of completeSeries.seriesSpells) { const encryptedName: string = System.encryptDataWithUserKey(spell.name, userEncryptionKey); const encryptedDescription: string = System.encryptDataWithUserKey(spell.description, userEncryptionKey); const encryptedAppearance: string = System.encryptDataWithUserKey(spell.appearance, userEncryptionKey); const encryptedTags: string = System.encryptDataWithUserKey(spell.tags, userEncryptionKey); const encryptedPowerLevel: string | null = spell.power_level ? System.encryptDataWithUserKey(spell.power_level, userEncryptionKey) : null; const encryptedComponents: string | null = spell.components ? System.encryptDataWithUserKey(spell.components, userEncryptionKey) : null; const encryptedLimitations: string | null = spell.limitations ? System.encryptDataWithUserKey(spell.limitations, userEncryptionKey) : null; const encryptedNotes: string | null = spell.notes ? System.encryptDataWithUserKey(spell.notes, userEncryptionKey) : null; const success: boolean = SeriesSpellRepo.insertSyncSeriesSpell( spell.spell_id, spell.series_id, userId, encryptedName, spell.name_hash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, spell.last_update, lang ); if (!success) return false; } // Save spell tags for (const tag of completeSeries.seriesSpellTags) { const encryptedName: string = System.encryptDataWithUserKey(tag.name, userEncryptionKey); const success: boolean = SeriesSpellRepo.insertSyncSeriesSpellTag( tag.tag_id, tag.series_id, userId, encryptedName, tag.hashed_name, tag.color, tag.last_update, lang ); if (!success) return false; } return true; } /** * Synchronizes a series from server to client, updating existing records or inserting new ones. * @param userId - The unique identifier of the user * @param completeSeries - The complete series data from the server * @param lang - The language for error messages ('fr' or 'en') * @returns True if sync was successful, false otherwise */ static syncSeriesFromServerToClient(userId: string, completeSeries: CompleteSeries, lang: 'fr' | 'en'): boolean { const userEncryptionKey: string = getUserEncryptionKey(userId); // Sync series for (const series of completeSeries.series) { const encryptedName: string = System.encryptDataWithUserKey(series.name, userEncryptionKey); const encryptedDescription: string | null = series.description ? System.encryptDataWithUserKey(series.description, userEncryptionKey) : null; const encryptedCoverImage: string | null = series.cover_image ? System.encryptDataWithUserKey(series.cover_image, userEncryptionKey) : null; const exists: boolean = SeriesRepo.seriesExists(userId, series.series_id, lang); if (exists) { const success: boolean = SeriesRepo.updateSyncSeries( userId, series.series_id, encryptedName, series.hashed_name, encryptedDescription, encryptedCoverImage, series.last_update, lang ); if (!success) return false; } else { const success: boolean = SeriesRepo.insertSyncSeries( series.series_id, userId, encryptedName, series.hashed_name, encryptedDescription, encryptedCoverImage, series.last_update, lang ); if (!success) return false; } } // Sync series books (only if the book exists locally) for (const seriesBook of completeSeries.seriesBooks) { const bookExists: boolean = BookRepo.isBookExist(userId, seriesBook.book_id, lang); if (!bookExists) continue; const success: boolean = SeriesRepo.insertSyncSeriesBook( seriesBook.series_id, seriesBook.book_id, seriesBook.book_order, seriesBook.last_update, lang ); if (!success) return false; } // Sync characters for (const character of completeSeries.seriesCharacters) { const encFirstName: string = System.encryptDataWithUserKey(character.first_name, userEncryptionKey); const encLastName: string | null = character.last_name ? System.encryptDataWithUserKey(character.last_name, userEncryptionKey) : null; const encNickname: string | null = character.nickname ? System.encryptDataWithUserKey(character.nickname, userEncryptionKey) : null; const encAge: string | null = character.age !== null ? System.encryptDataWithUserKey(String(character.age), userEncryptionKey) : null; const encGender: string | null = character.gender ? System.encryptDataWithUserKey(character.gender, userEncryptionKey) : null; const encSpecies: string | null = character.species ? System.encryptDataWithUserKey(character.species, userEncryptionKey) : null; const encNationality: string | null = character.nationality ? System.encryptDataWithUserKey(character.nationality, userEncryptionKey) : null; const encStatus: string | null = character.status ? System.encryptDataWithUserKey(character.status, userEncryptionKey) : null; const encTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userEncryptionKey) : null; const encCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey); const encImage: string | null = character.image ? System.encryptDataWithUserKey(character.image, userEncryptionKey) : null; const encRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userEncryptionKey) : null; const encBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userEncryptionKey) : null; const encHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userEncryptionKey) : null; const encSpeechPattern: string | null = character.speech_pattern ? System.encryptDataWithUserKey(character.speech_pattern, userEncryptionKey) : null; const encCatchphrase: string | null = character.catchphrase ? System.encryptDataWithUserKey(character.catchphrase, userEncryptionKey) : null; const encResidence: string | null = character.residence ? System.encryptDataWithUserKey(character.residence, userEncryptionKey) : null; const encNotes: string | null = character.notes ? System.encryptDataWithUserKey(character.notes, userEncryptionKey) : null; const encColor: string | null = character.color ? System.encryptDataWithUserKey(character.color, userEncryptionKey) : null; const exists: boolean = SeriesCharacterRepo.seriesCharacterExists(userId, character.character_id, lang); if (exists) { const success: boolean = SeriesCharacterRepo.updateSyncSeriesCharacter( userId, character.character_id, encFirstName, encLastName, encNickname, encAge, encGender, encSpecies, encNationality, encStatus, encCategory, encTitle, encImage, encRole, encBiography, encHistory, encSpeechPattern, encCatchphrase, encResidence, encNotes, encColor, character.last_update, lang ); if (!success) return false; } else { const success: boolean = SeriesCharacterRepo.insertSyncSeriesCharacter( character.character_id, character.series_id, userId, encFirstName, encLastName, encNickname, encAge, encGender, encSpecies, encNationality, encStatus, encCategory, encTitle, encImage, encRole, encBiography, encHistory, encSpeechPattern, encCatchphrase, encResidence, encNotes, encColor, character.last_update, lang ); if (!success) return false; } } // Sync character attributes for (const attr of completeSeries.seriesCharacterAttributes) { const encryptedName: string = System.encryptDataWithUserKey(attr.attribute_name, userEncryptionKey); const encryptedValue: string = System.encryptDataWithUserKey(attr.attribute_value, userEncryptionKey); const exists: boolean = SeriesCharacterRepo.seriesCharacterAttributeExists(userId, attr.attr_id, lang); if (exists) { const success: boolean = SeriesCharacterRepo.updateSyncSeriesCharacterAttribute( userId, attr.attr_id, encryptedName, encryptedValue, attr.last_update, lang ); if (!success) return false; } else { const success: boolean = SeriesCharacterRepo.insertSyncSeriesCharacterAttribute( attr.attr_id, attr.character_id, userId, encryptedName, encryptedValue, attr.last_update, lang ); if (!success) return false; } } // Sync worlds for (const world of completeSeries.seriesWorlds) { const encryptedName: string = System.encryptDataWithUserKey(world.name, userEncryptionKey); const encryptedHistory: string | null = world.history ? System.encryptDataWithUserKey(world.history, userEncryptionKey) : null; const encryptedPolitics: string | null = world.politics ? System.encryptDataWithUserKey(world.politics, userEncryptionKey) : null; const encryptedEconomy: string | null = world.economy ? System.encryptDataWithUserKey(world.economy, userEncryptionKey) : null; const encryptedReligion: string | null = world.religion ? System.encryptDataWithUserKey(world.religion, userEncryptionKey) : null; const encryptedLanguages: string | null = world.languages ? System.encryptDataWithUserKey(world.languages, userEncryptionKey) : null; const exists: boolean = SeriesWorldRepo.seriesWorldExists(userId, world.world_id, lang); if (exists) { const success: boolean = SeriesWorldRepo.updateSyncSeriesWorld( world.world_id, userId, encryptedName, encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, world.last_update, lang ); if (!success) return false; } else { const success: boolean = SeriesWorldRepo.insertSyncSeriesWorld( world.world_id, world.series_id, userId, encryptedName, world.hashed_name, encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, world.last_update, lang ); if (!success) return false; } } // Sync world elements for (const element of completeSeries.seriesWorldElements) { const encryptedName: string = System.encryptDataWithUserKey(element.name, userEncryptionKey); const encryptedDescription: string | null = element.description ? System.encryptDataWithUserKey(element.description, userEncryptionKey) : null; const exists: boolean = SeriesWorldRepo.seriesWorldElementExists(userId, element.element_id, lang); if (exists) { const success: boolean = SeriesWorldRepo.updateSyncSeriesWorldElement( element.element_id, userId, encryptedName, encryptedDescription, element.last_update, lang ); if (!success) return false; } else { const success: boolean = SeriesWorldRepo.insertSyncSeriesWorldElement( element.element_id, element.world_id, userId, element.element_type, encryptedName, element.original_name, encryptedDescription, element.last_update, lang ); if (!success) return false; } } // Sync locations for (const location of completeSeries.seriesLocations) { const encryptedName: string = System.encryptDataWithUserKey(location.loc_name, userEncryptionKey); const exists: boolean = SeriesLocationRepo.seriesLocationExists(userId, location.loc_id, lang); if (exists) { const success: boolean = SeriesLocationRepo.updateSyncSeriesLocation( location.loc_id, userId, encryptedName, location.last_update, lang ); if (!success) return false; } else { const success: boolean = SeriesLocationRepo.insertSyncSeriesLocation( location.loc_id, location.series_id, userId, encryptedName, location.loc_original_name, location.last_update, lang ); if (!success) return false; } } // Sync location elements for (const element of completeSeries.seriesLocationElements) { const encryptedName: string = System.encryptDataWithUserKey(element.element_name, userEncryptionKey); const encryptedDescription: string | null = element.element_description ? System.encryptDataWithUserKey(element.element_description, userEncryptionKey) : null; const exists: boolean = SeriesLocationRepo.seriesLocationElementExists(userId, element.element_id, lang); if (exists) { const success: boolean = SeriesLocationRepo.updateSyncSeriesLocationElement( element.element_id, userId, encryptedName, encryptedDescription, element.last_update, lang ); if (!success) return false; } else { const success: boolean = SeriesLocationRepo.insertSyncSeriesLocationElement( element.element_id, element.location_id, userId, encryptedName, element.original_name, encryptedDescription, element.last_update, lang ); if (!success) return false; } } // Sync location sub-elements for (const subElement of completeSeries.seriesLocationSubElements) { const encryptedName: string = System.encryptDataWithUserKey(subElement.sub_elem_name, userEncryptionKey); const encryptedDescription: string | null = subElement.sub_elem_description ? System.encryptDataWithUserKey(subElement.sub_elem_description, userEncryptionKey) : null; const exists: boolean = SeriesLocationRepo.seriesLocationSubElementExists(userId, subElement.sub_element_id, lang); if (exists) { const success: boolean = SeriesLocationRepo.updateSyncSeriesLocationSubElement( subElement.sub_element_id, userId, encryptedName, encryptedDescription, subElement.last_update, lang ); if (!success) return false; } else { const success: boolean = SeriesLocationRepo.insertSyncSeriesLocationSubElement( subElement.sub_element_id, subElement.element_id, userId, encryptedName, subElement.original_name, encryptedDescription, subElement.last_update, lang ); if (!success) return false; } } // Sync spells for (const spell of completeSeries.seriesSpells) { const encryptedName: string = System.encryptDataWithUserKey(spell.name, userEncryptionKey); const encryptedDescription: string = System.encryptDataWithUserKey(spell.description, userEncryptionKey); const encryptedAppearance: string = System.encryptDataWithUserKey(spell.appearance, userEncryptionKey); const encryptedTags: string = System.encryptDataWithUserKey(spell.tags, userEncryptionKey); const encryptedPowerLevel: string | null = spell.power_level ? System.encryptDataWithUserKey(spell.power_level, userEncryptionKey) : null; const encryptedComponents: string | null = spell.components ? System.encryptDataWithUserKey(spell.components, userEncryptionKey) : null; const encryptedLimitations: string | null = spell.limitations ? System.encryptDataWithUserKey(spell.limitations, userEncryptionKey) : null; const encryptedNotes: string | null = spell.notes ? System.encryptDataWithUserKey(spell.notes, userEncryptionKey) : null; const exists: boolean = SeriesSpellRepo.seriesSpellExists(userId, spell.spell_id, lang); if (exists) { const success: boolean = SeriesSpellRepo.updateSyncSeriesSpell( spell.spell_id, userId, encryptedName, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, spell.last_update, lang ); if (!success) return false; } else { const success: boolean = SeriesSpellRepo.insertSyncSeriesSpell( spell.spell_id, spell.series_id, userId, encryptedName, spell.name_hash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, spell.last_update, lang ); if (!success) return false; } } // Sync spell tags for (const tag of completeSeries.seriesSpellTags) { const encryptedName: string = System.encryptDataWithUserKey(tag.name, userEncryptionKey); const exists: boolean = SeriesSpellRepo.seriesSpellTagExists(userId, tag.tag_id, lang); if (exists) { const success: boolean = SeriesSpellRepo.updateSyncSeriesSpellTag( tag.tag_id, userId, encryptedName, tag.color, tag.last_update, lang ); if (!success) return false; } else { const success: boolean = SeriesSpellRepo.insertSyncSeriesSpellTag( tag.tag_id, tag.series_id, userId, encryptedName, tag.hashed_name, tag.color, tag.last_update, lang ); if (!success) return false; } } return true; } }