import { getUserEncryptionKey } from "../keyManager.js"; import System from "../System.js"; import SeriesSyncRepo, { SyncElementType } from "../repositories/series-sync.repo.js"; export interface SeriesSyncUploadPayload { type: SyncElementType; bookElementId: string; field: string; value: string; } export interface SeriesSyncResult { success: boolean; updatedCount: number; } export default class SeriesSync { /** * Uploads a field value from a book element to its linked series element, * and propagates the change to all other book elements linked to the same series element. * @param userId - The unique identifier of the user * @param payload - The upload payload containing type, bookElementId, field, and value * @param lang - The language for error messages ('fr' or 'en') * @returns The upload response */ public static uploadFieldToSeries(userId: string, payload: SeriesSyncUploadPayload, lang: 'fr' | 'en' = 'fr'): SeriesSyncResult { const { type, bookElementId, field, value } = payload; const seriesElementId: string | null = this.getSeriesLink(userId, type, bookElementId, lang); if (!seriesElementId) { throw new Error(lang === 'fr' ? `Cet élément n'est pas lié à une série.` : `This element is not linked to a series.`); } const userKey: string = getUserEncryptionKey(userId); const encryptedValue: string = value ? System.encryptDataWithUserKey(value, userKey) : ''; const dbField: string = this.mapFieldToDbColumn(type, field); this.updateSeriesElement(userId, type, seriesElementId, dbField, encryptedValue, lang); const updatedCount: number = this.updateLinkedBookElements(userId, type, seriesElementId, dbField, encryptedValue, lang); return { success: true, updatedCount: updatedCount + 1 }; } /** * Gets the series element ID linked to a book element. * @param userId - The unique identifier of the user * @param type - The type of element (character, world, location, spell) * @param bookElementId - The unique identifier of the book element * @param lang - The language for error messages ('fr' or 'en') * @returns The series element ID or null if not linked */ 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); } } /** * Maps frontend field names to database column names. * @param type - The type of element (character, world, location, spell) * @param field - The frontend field name to map * @returns The corresponding database column name */ private static mapFieldToDbColumn(type: SyncElementType, field: string): string { const characterFieldMap: Record = { 'name': 'first_name', 'firstName': 'first_name', 'lastName': 'last_name', 'nickname': 'nickname', 'age': 'age', 'gender': 'gender', 'species': 'species', 'nationality': 'nationality', 'status': 'status', 'title': 'title', 'category': 'category', 'role': 'role', 'biography': 'biography', 'history': 'history', 'speechPattern': 'speech_pattern', 'catchphrase': 'catchphrase', 'residence': 'residence', 'notes': 'notes', 'color': 'color' }; const worldFieldMap: Record = { 'name': 'name', 'history': 'history', 'politics': 'politics', 'economy': 'economy', 'religion': 'religion', 'languages': 'languages' }; const locationFieldMap: Record = { 'name': 'name', 'loc_name': 'loc_name' }; const spellFieldMap: Record = { 'name': 'name', 'description': 'description', 'type': 'type', 'level': 'level', 'range': 'range', 'duration': 'duration', 'cost': 'cost', 'effect': 'effect', 'components': 'components', 'notes': 'notes' }; switch (type) { case 'character': return characterFieldMap[field] || field; case 'world': return worldFieldMap[field] || field; case 'location': return locationFieldMap[field] || field; case 'spell': return spellFieldMap[field] || field; } } /** * Updates the series element field. * @param userId - The unique identifier of the user * @param type - The type of element (character, world, location, spell) * @param seriesElementId - The unique identifier of the series element * @param field - The database column name to update * @param encryptedValue - The encrypted value to set * @param lang - The language for error messages ('fr' or 'en') * @returns True if updated successfully */ 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); } } /** * Updates all book elements linked to the series element. * @param userId - The unique identifier of the user * @param type - The type of element (character, world, location, spell) * @param seriesElementId - The unique identifier of the series element * @param field - The database column name to update * @param encryptedValue - The encrypted value to set * @param lang - The language for error messages ('fr' or 'en') * @returns The number of book elements updated */ private static updateLinkedBookElements(userId: string, type: SyncElementType, seriesElementId: string, field: string, encryptedValue: string, lang: 'fr' | 'en'): number { const bookField: string = this.mapSeriesFieldToBookField(type, field); switch (type) { case 'character': return SeriesSyncRepo.updateLinkedBookCharactersField(userId, seriesElementId, bookField, encryptedValue, lang); case 'world': return SeriesSyncRepo.updateLinkedBookWorldsField(userId, seriesElementId, bookField, encryptedValue, lang); case 'location': return SeriesSyncRepo.updateLinkedBookLocationsField(userId, seriesElementId, bookField, encryptedValue, lang); case 'spell': return SeriesSyncRepo.updateLinkedBookSpellsField(userId, seriesElementId, bookField, encryptedValue, lang); } } /** * Maps series table field names to book table field names (if different). * @param type - The type of element (character, world, location, spell) * @param seriesField - The series table field name * @returns The corresponding book table field name */ private static mapSeriesFieldToBookField(type: SyncElementType, seriesField: string): string { if (type === 'location') { if (seriesField === 'name') { return 'loc_name'; } } return seriesField; } }