Files
ERitors-Scribe-Desktop/electron/database/models/SeriesSync.ts
natreex 209dc6f85a Remove CharacterComponent and CharacterDetail components
- Deleted `CharacterComponent` and `CharacterDetail` files from the project.
- Refactored related logic to improve code maintainability and reduce redundancy.
2026-02-05 14:12:08 -05:00

1024 lines
50 KiB
TypeScript

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<string, Record<string, string>> = {
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<string, Record<string, string>> = {
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;
}
}