Introduce series management functionality and repository updates
- Added `series-world.repo.ts` to handle database operations related to series worlds and their elements. - Implemented `series-sync.repo.ts` for managing synchronization between books and series. - Expanded `spell.ipc.ts` data models to include `seriesSpellId` for spell synchronization. - Refactored `insertSpellTag` method in `spelltag.repo.ts` for improved error handling and logic clarity.
This commit is contained in:
@@ -39,6 +39,10 @@ import UserRepo from "../repositories/user.repository.js";
|
||||
|
||||
export interface SyncedBookTools {
|
||||
lastUpdate: number;
|
||||
charactersEnabled: boolean;
|
||||
worldsEnabled: boolean;
|
||||
locationsEnabled: boolean;
|
||||
spellsEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface BookToolsSettings {
|
||||
@@ -55,11 +59,12 @@ export interface BookProps {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
summary?: string;
|
||||
serieId?: number;
|
||||
desiredReleaseDate?: string;
|
||||
desiredWordCount?: number;
|
||||
serieId?: number | null;
|
||||
seriesId?: string | null;
|
||||
desiredReleaseDate?: string | null;
|
||||
desiredWordCount?: number | null;
|
||||
wordCount?: number;
|
||||
coverImage?: string;
|
||||
coverImage?: string | null;
|
||||
bookMeta?: string;
|
||||
tools?: BookToolsSettings;
|
||||
}
|
||||
@@ -145,6 +150,234 @@ export interface CompleteBookData {
|
||||
chapters: CompleteChapterContent[];
|
||||
}
|
||||
|
||||
// ===== SERIES TABLE INTERFACES (for sync) =====
|
||||
|
||||
export interface SeriesTable {
|
||||
series_id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
hashed_name: string;
|
||||
description: string | null;
|
||||
cover_image: string | null;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface SeriesBooksTable {
|
||||
series_id: string;
|
||||
book_id: string;
|
||||
book_order: number;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface SeriesCharactersTable {
|
||||
character_id: string;
|
||||
series_id: string;
|
||||
user_id: string;
|
||||
first_name: string;
|
||||
last_name: string | null;
|
||||
nickname: string | null;
|
||||
age: number | null;
|
||||
gender: string | null;
|
||||
species: string | null;
|
||||
nationality: string | null;
|
||||
status: string | null;
|
||||
title: string | null;
|
||||
category: string;
|
||||
image: string | null;
|
||||
role: string | null;
|
||||
biography: string | null;
|
||||
history: string | null;
|
||||
speech_pattern: string | null;
|
||||
catchphrase: string | null;
|
||||
residence: string | null;
|
||||
notes: string | null;
|
||||
color: string | null;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface SeriesCharacterAttributesTable {
|
||||
attr_id: string;
|
||||
character_id: string;
|
||||
user_id: string;
|
||||
attribute_name: string;
|
||||
attribute_value: string;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface SeriesWorldsTable {
|
||||
world_id: string;
|
||||
series_id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
hashed_name: string;
|
||||
history: string | null;
|
||||
politics: string | null;
|
||||
economy: string | null;
|
||||
religion: string | null;
|
||||
languages: string | null;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface SeriesWorldElementsTable {
|
||||
element_id: string;
|
||||
world_id: string;
|
||||
user_id: string;
|
||||
element_type: number;
|
||||
name: string;
|
||||
original_name: string;
|
||||
description: string | null;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface SeriesLocationsTable {
|
||||
loc_id: string;
|
||||
series_id: string;
|
||||
user_id: string;
|
||||
loc_name: string;
|
||||
loc_original_name: string;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface SeriesLocationElementsTable {
|
||||
element_id: string;
|
||||
location_id: string;
|
||||
user_id: string;
|
||||
element_name: string;
|
||||
original_name: string;
|
||||
element_description: string | null;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface SeriesLocationSubElementsTable {
|
||||
sub_element_id: string;
|
||||
element_id: string;
|
||||
user_id: string;
|
||||
sub_elem_name: string;
|
||||
original_name: string;
|
||||
sub_elem_description: string | null;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface SeriesSpellsTable {
|
||||
spell_id: string;
|
||||
series_id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
name_hash: string;
|
||||
description: string;
|
||||
appearance: string;
|
||||
tags: string;
|
||||
power_level: string | null;
|
||||
components: string | null;
|
||||
limitations: string | null;
|
||||
notes: string | null;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface SeriesSpellTagsTable {
|
||||
tag_id: string;
|
||||
series_id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
hashed_name: string;
|
||||
color: string | null;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
// ===== COMPLETE SERIES INTERFACE (for full sync) =====
|
||||
|
||||
export interface CompleteSeries {
|
||||
series: SeriesTable[];
|
||||
seriesBooks: SeriesBooksTable[];
|
||||
seriesCharacters: SeriesCharactersTable[];
|
||||
seriesCharacterAttributes: SeriesCharacterAttributesTable[];
|
||||
seriesWorlds: SeriesWorldsTable[];
|
||||
seriesWorldElements: SeriesWorldElementsTable[];
|
||||
seriesLocations: SeriesLocationsTable[];
|
||||
seriesLocationElements: SeriesLocationElementsTable[];
|
||||
seriesLocationSubElements: SeriesLocationSubElementsTable[];
|
||||
seriesSpells: SeriesSpellsTable[];
|
||||
seriesSpellTags: SeriesSpellTagsTable[];
|
||||
}
|
||||
|
||||
// ===== SYNCED SERIES INTERFACES (lightweight, for comparison) =====
|
||||
|
||||
export interface SyncedSeriesBook {
|
||||
bookId: string;
|
||||
order: number;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export interface SyncedSeriesCharacterAttribute {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export interface SyncedSeriesCharacter {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
attributes: SyncedSeriesCharacterAttribute[];
|
||||
}
|
||||
|
||||
export interface SyncedSeriesWorldElement {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export interface SyncedSeriesWorld {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
elements: SyncedSeriesWorldElement[];
|
||||
}
|
||||
|
||||
export interface SyncedSeriesLocationSubElement {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export interface SyncedSeriesLocationElement {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
subElements: SyncedSeriesLocationSubElement[];
|
||||
}
|
||||
|
||||
export interface SyncedSeriesLocation {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
elements: SyncedSeriesLocationElement[];
|
||||
}
|
||||
|
||||
export interface SyncedSeriesSpell {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export interface SyncedSeriesSpellTag {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export interface SyncedSeries {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
books: SyncedSeriesBook[];
|
||||
characters: SyncedSeriesCharacter[];
|
||||
worlds: SyncedSeriesWorld[];
|
||||
locations: SyncedSeriesLocation[];
|
||||
spells: SyncedSeriesSpell[];
|
||||
spellTags: SyncedSeriesSpellTag[];
|
||||
}
|
||||
|
||||
export default class Book {
|
||||
private readonly id: string;
|
||||
private type: string;
|
||||
|
||||
@@ -14,11 +14,11 @@ export interface CharacterPropsPost {
|
||||
name: string;
|
||||
lastName: string;
|
||||
nickname: string;
|
||||
age: string;
|
||||
age: number | null;
|
||||
gender: string;
|
||||
species: string;
|
||||
nationality: string;
|
||||
status: 'alive' | 'dead' | 'unknown';
|
||||
status: string;
|
||||
category: CharacterCategory;
|
||||
title: string;
|
||||
image: string;
|
||||
@@ -48,6 +48,7 @@ export interface CharacterPropsPost {
|
||||
residence?: string;
|
||||
notes?: string;
|
||||
color?: string;
|
||||
seriesCharacterId?: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +57,7 @@ export interface CharacterProps {
|
||||
name: string;
|
||||
lastName: string;
|
||||
nickname: string;
|
||||
age: string;
|
||||
age: number | null;
|
||||
gender: string;
|
||||
species: string;
|
||||
nationality: string;
|
||||
@@ -72,6 +73,7 @@ export interface CharacterProps {
|
||||
residence: string;
|
||||
notes: string;
|
||||
color: string;
|
||||
seriesCharacterId: string | null;
|
||||
}
|
||||
|
||||
export interface CharacterListResponse {
|
||||
@@ -83,24 +85,24 @@ export interface CompleteCharacterProps {
|
||||
id?: string;
|
||||
name: string;
|
||||
lastName: string;
|
||||
nickname?: string;
|
||||
age?: string;
|
||||
gender?: string;
|
||||
species?: string;
|
||||
nationality?: string;
|
||||
status?: string;
|
||||
nickname: string;
|
||||
age: number | null;
|
||||
gender: string;
|
||||
species: string;
|
||||
nationality: string;
|
||||
status: string;
|
||||
title: string;
|
||||
category: string;
|
||||
image?: string;
|
||||
role: string;
|
||||
biography: string;
|
||||
history: string;
|
||||
speechPattern?: string;
|
||||
catchphrase?: string;
|
||||
residence?: string;
|
||||
notes?: string;
|
||||
color?: string;
|
||||
[key: string]: Attribute[] | string | undefined;
|
||||
speechPattern: string;
|
||||
catchphrase: string;
|
||||
residence: string;
|
||||
notes: string;
|
||||
color: string;
|
||||
[key: string]: Attribute[] | string | number | null | undefined;
|
||||
}
|
||||
|
||||
export interface Attribute {
|
||||
@@ -152,7 +154,7 @@ export default class Character {
|
||||
name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '',
|
||||
lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '',
|
||||
nickname: encryptedCharacter.nickname ? System.decryptDataWithUserKey(encryptedCharacter.nickname, userEncryptionKey) : '',
|
||||
age: encryptedCharacter.age ? System.decryptDataWithUserKey(encryptedCharacter.age, userEncryptionKey) : '',
|
||||
age: encryptedCharacter.age ? parseInt(System.decryptDataWithUserKey(encryptedCharacter.age, userEncryptionKey), 10) : null,
|
||||
gender: encryptedCharacter.gender ? System.decryptDataWithUserKey(encryptedCharacter.gender, userEncryptionKey) : '',
|
||||
species: encryptedCharacter.species ? System.decryptDataWithUserKey(encryptedCharacter.species, userEncryptionKey) : '',
|
||||
nationality: encryptedCharacter.nationality ? System.decryptDataWithUserKey(encryptedCharacter.nationality, userEncryptionKey) : '',
|
||||
@@ -168,6 +170,7 @@ export default class Character {
|
||||
residence: encryptedCharacter.residence ? System.decryptDataWithUserKey(encryptedCharacter.residence, userEncryptionKey) : '',
|
||||
notes: encryptedCharacter.notes ? System.decryptDataWithUserKey(encryptedCharacter.notes, userEncryptionKey) : '',
|
||||
color: encryptedCharacter.color ? System.decryptDataWithUserKey(encryptedCharacter.color, userEncryptionKey) : '',
|
||||
seriesCharacterId: encryptedCharacter.series_character_id || null,
|
||||
})
|
||||
}
|
||||
return { characters: decryptedCharacterList, enabled };
|
||||
@@ -191,7 +194,7 @@ export default class Character {
|
||||
firstName: System.encryptDataWithUserKey(character.name, userEncryptionKey),
|
||||
lastName: System.encryptDataWithUserKey(character.lastName, userEncryptionKey),
|
||||
nickname: System.encryptDataWithUserKey(character.nickname || '', userEncryptionKey),
|
||||
age: System.encryptDataWithUserKey(character.age || '', userEncryptionKey),
|
||||
age: character.age !== null ? System.encryptDataWithUserKey(String(character.age), userEncryptionKey) : '',
|
||||
gender: System.encryptDataWithUserKey(character.gender || '', userEncryptionKey),
|
||||
species: System.encryptDataWithUserKey(character.species || '', userEncryptionKey),
|
||||
nationality: System.encryptDataWithUserKey(character.nationality || '', userEncryptionKey),
|
||||
@@ -209,7 +212,7 @@ export default class Character {
|
||||
color: System.encryptDataWithUserKey(character.color || '', userEncryptionKey),
|
||||
};
|
||||
|
||||
CharacterRepo.addNewCharacter(userId, characterId, characterData, bookId, lang);
|
||||
CharacterRepo.addNewCharacter(userId, characterId, characterData, bookId, lang, character.seriesCharacterId || null);
|
||||
const characterPropertyKeys: string[] = Object.keys(character);
|
||||
for (const propertyKey of characterPropertyKeys) {
|
||||
if (Array.isArray(character[propertyKey as keyof CharacterPropsPost])) {
|
||||
@@ -244,7 +247,7 @@ export default class Character {
|
||||
firstName: System.encryptDataWithUserKey(character.name, userEncryptionKey),
|
||||
lastName: System.encryptDataWithUserKey(character.lastName, userEncryptionKey),
|
||||
nickname: System.encryptDataWithUserKey(character.nickname || '', userEncryptionKey),
|
||||
age: System.encryptDataWithUserKey(character.age || '', userEncryptionKey),
|
||||
age: character.age !== null ? System.encryptDataWithUserKey(String(character.age), userEncryptionKey) : '',
|
||||
gender: System.encryptDataWithUserKey(character.gender || '', userEncryptionKey),
|
||||
species: System.encryptDataWithUserKey(character.species || '', userEncryptionKey),
|
||||
nationality: System.encryptDataWithUserKey(character.nationality || '', userEncryptionKey),
|
||||
@@ -262,7 +265,7 @@ export default class Character {
|
||||
color: System.encryptDataWithUserKey(character.color || '', userEncryptionKey),
|
||||
};
|
||||
|
||||
return CharacterRepo.updateCharacter(userId, character.id, characterData, System.timeStampInSeconds(), lang);
|
||||
return CharacterRepo.updateCharacter(userId, character.id, characterData, System.timeStampInSeconds(), lang, character.seriesCharacterId || null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -370,7 +373,7 @@ export default class Character {
|
||||
name: encryptedCharacter.first_name ? System.decryptDataWithUserKey(encryptedCharacter.first_name, userEncryptionKey) : '',
|
||||
lastName: encryptedCharacter.last_name ? System.decryptDataWithUserKey(encryptedCharacter.last_name, userEncryptionKey) : '',
|
||||
nickname: encryptedCharacter.nickname ? System.decryptDataWithUserKey(encryptedCharacter.nickname as string, userEncryptionKey) : '',
|
||||
age: encryptedCharacter.age ? System.decryptDataWithUserKey(encryptedCharacter.age as string, userEncryptionKey) : '',
|
||||
age: encryptedCharacter.age ? parseInt(System.decryptDataWithUserKey(encryptedCharacter.age as string, userEncryptionKey), 10) : null,
|
||||
gender: encryptedCharacter.gender ? System.decryptDataWithUserKey(encryptedCharacter.gender as string, userEncryptionKey) : '',
|
||||
species: encryptedCharacter.species ? System.decryptDataWithUserKey(encryptedCharacter.species as string, userEncryptionKey) : '',
|
||||
nationality: encryptedCharacter.nationality ? System.decryptDataWithUserKey(encryptedCharacter.nationality as string, userEncryptionKey) : '',
|
||||
@@ -442,11 +445,22 @@ export default class Character {
|
||||
uniqueCharactersMap.set(characterIdentifier, {
|
||||
name: character.name,
|
||||
lastName: character.lastName,
|
||||
category: character.category,
|
||||
nickname: character.nickname,
|
||||
age: character.age,
|
||||
gender: character.gender,
|
||||
species: character.species,
|
||||
nationality: character.nationality,
|
||||
status: character.status,
|
||||
title: character.title,
|
||||
category: character.category,
|
||||
role: character.role,
|
||||
biography: character.biography,
|
||||
history: character.history
|
||||
history: character.history,
|
||||
speechPattern: character.speechPattern,
|
||||
catchphrase: character.catchphrase,
|
||||
residence: character.residence,
|
||||
notes: character.notes,
|
||||
color: character.color
|
||||
});
|
||||
}
|
||||
|
||||
@@ -472,7 +486,7 @@ export default class Character {
|
||||
});
|
||||
|
||||
Object.keys(character).forEach((propertyKey: string): void => {
|
||||
const propertyValue: string | Attribute[] | undefined = character[propertyKey];
|
||||
const propertyValue = character[propertyKey];
|
||||
if (Array.isArray(propertyValue) && propertyValue.length > 0) {
|
||||
const capitalizedPropertyKey: string = propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1);
|
||||
const formattedAttributeValues: string = propertyValue.map((attributeItem: Attribute) => attributeItem.name).join(', ');
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface LocationProps {
|
||||
id: string;
|
||||
name: string;
|
||||
elements: Element[];
|
||||
seriesLocationId?: string | null;
|
||||
}
|
||||
|
||||
export interface LocationListResponse {
|
||||
@@ -79,7 +80,8 @@ export default class Location {
|
||||
location = {
|
||||
id: record.loc_id,
|
||||
name: decryptedName,
|
||||
elements: []
|
||||
elements: [],
|
||||
seriesLocationId: record.series_location_id || null,
|
||||
};
|
||||
locationArray.push(location);
|
||||
}
|
||||
@@ -127,12 +129,12 @@ export default class Location {
|
||||
* @param existingLocationId - Optional existing location ID to use instead of generating a new one.
|
||||
* @returns The ID of the created location.
|
||||
*/
|
||||
static addLocationSection(userId: string, locationName: string, bookId: string, lang: 'fr' | 'en' = 'fr', existingLocationId?: string): string {
|
||||
static addLocationSection(userId: string, locationName: string, bookId: string, lang: 'fr' | 'en' = 'fr', existingLocationId?: string, seriesLocationId: string | null = null): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const hashedName: string = System.hashElement(locationName);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(locationName, userKey);
|
||||
const locationId: string = existingLocationId || System.createUniqueId();
|
||||
return LocationRepo.insertLocation(userId, locationId, bookId, encryptedName, hashedName, lang);
|
||||
return LocationRepo.insertLocation(userId, locationId, bookId, encryptedName, hashedName, lang, seriesLocationId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,6 +204,28 @@ export default class Location {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a location section with optional name change and series link.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param sectionId - The unique identifier of the section
|
||||
* @param sectionName - The new name (optional)
|
||||
* @param seriesLocationId - The series location ID to link (optional, null to unlink)
|
||||
* @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'.
|
||||
* @returns True if the update was successful
|
||||
*/
|
||||
static updateSectionWithSeriesLink(userId: string, sectionId: string, sectionName?: string, seriesLocationId?: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
let encryptedName: string | null = null;
|
||||
let originalNameHash: string | null = null;
|
||||
|
||||
if (sectionName) {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
encryptedName = System.encryptDataWithUserKey(sectionName, userKey);
|
||||
originalNameHash = System.hashElement(sectionName);
|
||||
}
|
||||
|
||||
return LocationRepo.updateSectionWithSeriesLink(userId, sectionId, encryptedName, originalNameHash, seriesLocationId ?? null, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a location section and all its associated elements and sub-elements.
|
||||
* @param userId - The user's unique identifier.
|
||||
|
||||
248
electron/database/models/Series.ts
Normal file
248
electron/database/models/Series.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import { getUserEncryptionKey } from "../keyManager.js";
|
||||
import System from "../System.js";
|
||||
import SeriesRepo, { SeriesBookResult, SeriesListItem, SeriesResult } from "../repositories/series.repo.js";
|
||||
|
||||
export interface SeriesProps {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
coverImage: string | null;
|
||||
}
|
||||
|
||||
export interface SeriesDetailProps {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
coverImage: string | null;
|
||||
books: SeriesBookProps[];
|
||||
}
|
||||
|
||||
export interface SeriesBookProps {
|
||||
bookId: string;
|
||||
title: string;
|
||||
order: number;
|
||||
coverImage: string | null;
|
||||
}
|
||||
|
||||
export interface SeriesListItemProps {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
coverImage: string | null;
|
||||
bookCount: number;
|
||||
bookIds: string[];
|
||||
}
|
||||
|
||||
export interface BooksOrderPost {
|
||||
bookId: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export default class Series {
|
||||
/**
|
||||
* Gets the list of all series for a user.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The list of series with decrypted names and descriptions
|
||||
*/
|
||||
public static getSeriesList(userId: string, lang: 'fr' | 'en' = 'fr'): SeriesListItemProps[] {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const seriesResults: SeriesListItem[] = SeriesRepo.fetchUserSeries(userId, lang);
|
||||
|
||||
return seriesResults.map((seriesItem: SeriesListItem): SeriesListItemProps => ({
|
||||
id: seriesItem.series_id,
|
||||
name: System.decryptDataWithUserKey(seriesItem.name, userKey),
|
||||
description: seriesItem.description ? System.decryptDataWithUserKey(seriesItem.description, userKey) : '',
|
||||
coverImage: seriesItem.cover_image,
|
||||
bookCount: seriesItem.book_count,
|
||||
bookIds: seriesItem.book_ids ? seriesItem.book_ids.split(',') : []
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the detail of a series including its books.
|
||||
* @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 The series detail with decrypted data
|
||||
*/
|
||||
public static getSeriesDetail(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesDetailProps {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const seriesResult: SeriesResult | null = SeriesRepo.fetchSeriesById(userId, seriesId, lang);
|
||||
if (!seriesResult) {
|
||||
throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
|
||||
}
|
||||
|
||||
const booksResult: SeriesBookResult[] = SeriesRepo.fetchSeriesBooks(userId, seriesId, lang);
|
||||
|
||||
const books: SeriesBookProps[] = booksResult.map((book: SeriesBookResult) => ({
|
||||
bookId: book.book_id,
|
||||
title: System.decryptDataWithUserKey(book.title, userKey),
|
||||
order: book.book_order,
|
||||
coverImage: book.cover_image
|
||||
}));
|
||||
|
||||
return {
|
||||
id: seriesResult.series_id,
|
||||
name: System.decryptDataWithUserKey(seriesResult.name, userKey),
|
||||
description: seriesResult.description ? System.decryptDataWithUserKey(seriesResult.description, userKey) : '',
|
||||
coverImage: seriesResult.cover_image,
|
||||
books
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new series.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param name - The name of the series
|
||||
* @param description - The description of the series
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @param bookIds - Optional array of book IDs to add to the series
|
||||
* @returns The created series ID
|
||||
*/
|
||||
public static createSeries(userId: string, name: string, description: string, lang: 'fr' | 'en' = 'fr', bookIds?: string[]): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const seriesId: string = System.createUniqueId();
|
||||
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const hashedName: string = System.hashElement(name);
|
||||
const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null;
|
||||
|
||||
SeriesRepo.insertSeries(seriesId, userId, encryptedName, hashedName, encryptedDescription, lang);
|
||||
|
||||
if (bookIds && bookIds.length > 0) {
|
||||
for (let i: number = 0; i < bookIds.length; i++) {
|
||||
SeriesRepo.addBookToSeries(seriesId, bookIds[i], i + 1, lang);
|
||||
}
|
||||
}
|
||||
|
||||
return seriesId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing series.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param seriesId - The unique identifier of the series
|
||||
* @param name - The name of the series
|
||||
* @param description - The description of the series
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the update was successful
|
||||
*/
|
||||
public static updateSeries(userId: string, seriesId: string, name: string, description: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
|
||||
if (!exists) {
|
||||
throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
|
||||
}
|
||||
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const hashedName: string = System.hashElement(name);
|
||||
const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null;
|
||||
|
||||
return SeriesRepo.updateSeries(userId, seriesId, encryptedName, hashedName, encryptedDescription, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a series.
|
||||
* @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 True if the deletion was successful
|
||||
*/
|
||||
public static deleteSeries(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
|
||||
if (!exists) {
|
||||
throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
|
||||
}
|
||||
|
||||
return SeriesRepo.deleteSeries(userId, seriesId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a book to a series.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param seriesId - The unique identifier of the series
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param order - The order of the book in the series
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the addition was successful
|
||||
*/
|
||||
public static addBookToSeries(userId: string, seriesId: string, bookId: string, order: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
|
||||
if (!exists) {
|
||||
throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
|
||||
}
|
||||
|
||||
return SeriesRepo.addBookToSeries(seriesId, bookId, order, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a book from a series.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param seriesId - The unique identifier of the series
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the removal was successful
|
||||
*/
|
||||
public static removeBookFromSeries(userId: string, seriesId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
|
||||
if (!exists) {
|
||||
throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
|
||||
}
|
||||
|
||||
return SeriesRepo.removeBookFromSeries(seriesId, bookId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the order of books in a series.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param seriesId - The unique identifier of the series
|
||||
* @param booksOrder - An array of {bookId, order} objects
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the update was successful
|
||||
*/
|
||||
public static updateBooksOrder(userId: string, seriesId: string, booksOrder: BooksOrderPost[], lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
|
||||
if (!exists) {
|
||||
throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
|
||||
}
|
||||
|
||||
return SeriesRepo.updateBooksOrder(seriesId, booksOrder, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the series ID for a book if it belongs to one.
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @returns The series ID or null
|
||||
*/
|
||||
public static getSeriesIdForBook(bookId: string): string | null {
|
||||
return SeriesRepo.getSeriesIdForBook(bookId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets only the books of a series (without series details).
|
||||
* @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 The list of books in the series
|
||||
*/
|
||||
public static getSeriesBooks(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesBookProps[] {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const exists: boolean = SeriesRepo.isSeriesExist(userId, seriesId, lang);
|
||||
if (!exists) {
|
||||
throw new Error(lang === 'fr' ? 'Série non trouvée.' : 'Series not found.');
|
||||
}
|
||||
|
||||
const booksResult: SeriesBookResult[] = SeriesRepo.fetchSeriesBooks(userId, seriesId, lang);
|
||||
|
||||
return booksResult.map((book: SeriesBookResult): SeriesBookProps => ({
|
||||
bookId: book.book_id,
|
||||
title: System.decryptDataWithUserKey(book.title, userKey),
|
||||
order: book.book_order,
|
||||
coverImage: book.cover_image
|
||||
}));
|
||||
}
|
||||
}
|
||||
276
electron/database/models/SeriesCharacter.ts
Normal file
276
electron/database/models/SeriesCharacter.ts
Normal file
@@ -0,0 +1,276 @@
|
||||
import { getUserEncryptionKey } from "../keyManager.js";
|
||||
import System from "../System.js";
|
||||
import SeriesCharacterRepo, { SeriesCharacterAttributeResult, SeriesCharacterResult } from "../repositories/series-character.repo.js";
|
||||
|
||||
export type CharacterCategory = 'Main' | 'Secondary' | 'Recurring';
|
||||
|
||||
export interface SeriesCharacterPropsPost {
|
||||
id: string | null;
|
||||
name: string;
|
||||
lastName: string;
|
||||
nickname: string;
|
||||
age: number | null;
|
||||
gender: string;
|
||||
species: string;
|
||||
nationality: string;
|
||||
status: string;
|
||||
category: CharacterCategory;
|
||||
title: string;
|
||||
image: string;
|
||||
physical: { name: string }[];
|
||||
psychological: { name: string }[];
|
||||
relations: { name: string }[];
|
||||
skills: { name: string }[];
|
||||
weaknesses: { name: string }[];
|
||||
strengths: { name: string }[];
|
||||
goals: { name: string }[];
|
||||
motivations: { name: string }[];
|
||||
arc: { name: string }[];
|
||||
secrets: { name: string }[];
|
||||
fears: { name: string }[];
|
||||
flaws: { name: string }[];
|
||||
beliefs: { name: string }[];
|
||||
conflicts: { name: string }[];
|
||||
quotes: { name: string }[];
|
||||
distinguishingMarks: { name: string }[];
|
||||
items: { name: string }[];
|
||||
affiliations: { name: string }[];
|
||||
role: string;
|
||||
biography?: string;
|
||||
history?: string;
|
||||
speechPattern?: string;
|
||||
catchphrase?: string;
|
||||
residence?: string;
|
||||
notes?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface SeriesCharacterListProps {
|
||||
id: string;
|
||||
name: string;
|
||||
lastName: string;
|
||||
nickname: string;
|
||||
age: number | null;
|
||||
gender: string;
|
||||
species: string;
|
||||
nationality: string;
|
||||
status: string;
|
||||
title: string;
|
||||
category: string;
|
||||
image: string;
|
||||
role: string;
|
||||
biography: string;
|
||||
history: string;
|
||||
speechPattern: string;
|
||||
catchphrase: string;
|
||||
residence: string;
|
||||
notes: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface SeriesAttribute {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface CharacterAttributesResponse {
|
||||
attributes: SeriesAttribute[];
|
||||
}
|
||||
|
||||
export default class SeriesCharacter {
|
||||
/**
|
||||
* Retrieves a list of characters for a specific series owned by a user.
|
||||
* @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 Characters list
|
||||
*/
|
||||
public static getCharacterList(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterListProps[] {
|
||||
const characters: SeriesCharacterResult[] = SeriesCharacterRepo.fetchCharacters(userId, seriesId, lang);
|
||||
if (!characters || characters.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
return characters.map((character: SeriesCharacterResult): SeriesCharacterListProps => ({
|
||||
id: character.character_id,
|
||||
name: character.first_name ? System.decryptDataWithUserKey(character.first_name, userKey) : '',
|
||||
lastName: character.last_name ? System.decryptDataWithUserKey(character.last_name, userKey) : '',
|
||||
nickname: character.nickname ? System.decryptDataWithUserKey(character.nickname, userKey) : '',
|
||||
age: character.age ? parseInt(System.decryptDataWithUserKey(character.age, userKey), 10) : null,
|
||||
gender: character.gender ? System.decryptDataWithUserKey(character.gender, userKey) : '',
|
||||
species: character.species ? System.decryptDataWithUserKey(character.species, userKey) : '',
|
||||
nationality: character.nationality ? System.decryptDataWithUserKey(character.nationality, userKey) : '',
|
||||
status: character.status ? System.decryptDataWithUserKey(character.status, userKey) : 'alive',
|
||||
title: character.title ? System.decryptDataWithUserKey(character.title, userKey) : '',
|
||||
category: character.category ? System.decryptDataWithUserKey(character.category, userKey) : '',
|
||||
image: character.image ? System.decryptDataWithUserKey(character.image, userKey) : '',
|
||||
role: character.role ? System.decryptDataWithUserKey(character.role, userKey) : '',
|
||||
biography: character.biography ? System.decryptDataWithUserKey(character.biography, userKey) : '',
|
||||
history: character.history ? System.decryptDataWithUserKey(character.history, userKey) : '',
|
||||
speechPattern: character.speech_pattern ? System.decryptDataWithUserKey(character.speech_pattern, userKey) : '',
|
||||
catchphrase: character.catchphrase ? System.decryptDataWithUserKey(character.catchphrase, userKey) : '',
|
||||
residence: character.residence ? System.decryptDataWithUserKey(character.residence, userKey) : '',
|
||||
notes: character.notes ? System.decryptDataWithUserKey(character.notes, userKey) : '',
|
||||
color: character.color ? System.decryptDataWithUserKey(character.color, userKey) : '',
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new character to a series with all its attributes.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param character - The character data to create
|
||||
* @param seriesId - The unique identifier of the series
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The newly created character's ID
|
||||
*/
|
||||
public static addNewCharacter(userId: string, character: SeriesCharacterPropsPost, seriesId: string, lang: 'fr' | 'en' = 'fr'): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const characterId: string = System.createUniqueId();
|
||||
const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey);
|
||||
const encryptedLastName: string | null = character.lastName ? System.encryptDataWithUserKey(character.lastName, userKey) : null;
|
||||
const encryptedNickname: string | null = character.nickname ? System.encryptDataWithUserKey(character.nickname, userKey) : null;
|
||||
const encryptedAge: string | null = character.age !== null ? System.encryptDataWithUserKey(String(character.age), userKey) : null;
|
||||
const encryptedGender: string | null = character.gender ? System.encryptDataWithUserKey(character.gender, userKey) : null;
|
||||
const encryptedSpecies: string | null = character.species ? System.encryptDataWithUserKey(character.species, userKey) : null;
|
||||
const encryptedNationality: string | null = character.nationality ? System.encryptDataWithUserKey(character.nationality, userKey) : null;
|
||||
const encryptedStatus: string | null = character.status ? System.encryptDataWithUserKey(character.status, userKey) : null;
|
||||
const encryptedTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userKey) : null;
|
||||
const encryptedCategory: string | null = character.category ? System.encryptDataWithUserKey(character.category, userKey) : null;
|
||||
const encryptedImage: string | null = character.image ? System.encryptDataWithUserKey(character.image, userKey) : null;
|
||||
const encryptedRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userKey) : null;
|
||||
const encryptedBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userKey) : null;
|
||||
const encryptedHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userKey) : null;
|
||||
const encryptedSpeechPattern: string | null = character.speechPattern ? System.encryptDataWithUserKey(character.speechPattern, userKey) : null;
|
||||
const encryptedCatchphrase: string | null = character.catchphrase ? System.encryptDataWithUserKey(character.catchphrase, userKey) : null;
|
||||
const encryptedResidence: string | null = character.residence ? System.encryptDataWithUserKey(character.residence, userKey) : null;
|
||||
const encryptedNotes: string | null = character.notes ? System.encryptDataWithUserKey(character.notes, userKey) : null;
|
||||
const encryptedColor: string | null = character.color ? System.encryptDataWithUserKey(character.color, userKey) : null;
|
||||
|
||||
SeriesCharacterRepo.addNewCharacter(userId, characterId, encryptedName, encryptedLastName, encryptedNickname, encryptedAge, encryptedGender, encryptedSpecies, encryptedNationality, encryptedStatus, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, encryptedSpeechPattern, encryptedCatchphrase, encryptedResidence, encryptedNotes, encryptedColor, seriesId, lang);
|
||||
|
||||
const attributeKeys: string[] = Object.keys(character);
|
||||
for (const attributeKey of attributeKeys) {
|
||||
const attributeValue = character[attributeKey as keyof SeriesCharacterPropsPost];
|
||||
if (Array.isArray(attributeValue)) {
|
||||
const attributeArray: { name: string }[] = attributeValue;
|
||||
if (attributeArray.length > 0) {
|
||||
for (const attributeItem of attributeArray) {
|
||||
const attributeType: string = attributeKey;
|
||||
const attributeName: string = attributeItem.name;
|
||||
this.addNewAttribute(characterId, userId, attributeType, attributeName, lang);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return characterId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing character's information and attributes.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param character - The updated character data
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the update was successful
|
||||
*/
|
||||
public static updateCharacter(userId: string, character: SeriesCharacterPropsPost, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
if (!character.id) {
|
||||
throw new Error(lang === 'fr' ? 'ID du personnage requis.' : 'Character ID required.');
|
||||
}
|
||||
|
||||
const exists: boolean = SeriesCharacterRepo.isCharacterExist(userId, character.id, lang);
|
||||
if (!exists) {
|
||||
throw new Error(lang === 'fr' ? 'Personnage non trouvé.' : 'Character not found.');
|
||||
}
|
||||
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(character.name, userKey);
|
||||
const encryptedLastName: string | null = character.lastName ? System.encryptDataWithUserKey(character.lastName, userKey) : null;
|
||||
const encryptedNickname: string | null = character.nickname ? System.encryptDataWithUserKey(character.nickname, userKey) : null;
|
||||
const encryptedAge: string | null = character.age !== null ? System.encryptDataWithUserKey(String(character.age), userKey) : null;
|
||||
const encryptedGender: string | null = character.gender ? System.encryptDataWithUserKey(character.gender, userKey) : null;
|
||||
const encryptedSpecies: string | null = character.species ? System.encryptDataWithUserKey(character.species, userKey) : null;
|
||||
const encryptedNationality: string | null = character.nationality ? System.encryptDataWithUserKey(character.nationality, userKey) : null;
|
||||
const encryptedStatus: string | null = character.status ? System.encryptDataWithUserKey(character.status, userKey) : null;
|
||||
const encryptedTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userKey) : null;
|
||||
const encryptedCategory: string | null = character.category ? System.encryptDataWithUserKey(character.category, userKey) : null;
|
||||
const encryptedImage: string | null = character.image ? System.encryptDataWithUserKey(character.image, userKey) : null;
|
||||
const encryptedRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userKey) : null;
|
||||
const encryptedBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userKey) : null;
|
||||
const encryptedHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userKey) : null;
|
||||
const encryptedSpeechPattern: string | null = character.speechPattern ? System.encryptDataWithUserKey(character.speechPattern, userKey) : null;
|
||||
const encryptedCatchphrase: string | null = character.catchphrase ? System.encryptDataWithUserKey(character.catchphrase, userKey) : null;
|
||||
const encryptedResidence: string | null = character.residence ? System.encryptDataWithUserKey(character.residence, userKey) : null;
|
||||
const encryptedNotes: string | null = character.notes ? System.encryptDataWithUserKey(character.notes, userKey) : null;
|
||||
const encryptedColor: string | null = character.color ? System.encryptDataWithUserKey(character.color, userKey) : null;
|
||||
|
||||
return SeriesCharacterRepo.updateCharacter(userId, character.id, encryptedName, encryptedLastName, encryptedNickname, encryptedAge, encryptedGender, encryptedSpecies, encryptedNationality, encryptedStatus, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, encryptedSpeechPattern, encryptedCatchphrase, encryptedResidence, encryptedNotes, encryptedColor, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a character from a series.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param characterId - The unique identifier of the character
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the deletion was successful
|
||||
*/
|
||||
public static deleteCharacter(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const exists: boolean = SeriesCharacterRepo.isCharacterExist(userId, characterId, lang);
|
||||
if (!exists) {
|
||||
throw new Error(lang === 'fr' ? 'Personnage non trouvé.' : 'Character not found.');
|
||||
}
|
||||
return SeriesCharacterRepo.deleteCharacter(userId, characterId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new attribute to a character.
|
||||
* @param characterId - The unique identifier of the character
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param type - The attribute type
|
||||
* @param name - The attribute value
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The attribute ID
|
||||
*/
|
||||
public static addNewAttribute(characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr'): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const attributeId: string = System.createUniqueId();
|
||||
const encryptedType: string = System.encryptDataWithUserKey(type, userKey);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
|
||||
SeriesCharacterRepo.insertAttribute(attributeId, characterId, userId, encryptedType, encryptedName, lang);
|
||||
|
||||
return attributeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an attribute from a character.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param attributeId - The unique identifier of the attribute
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the deletion was successful
|
||||
*/
|
||||
public static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return SeriesCharacterRepo.deleteAttribute(userId, attributeId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all attributes for a character.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param characterId - The unique identifier of the character
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The character's attributes
|
||||
*/
|
||||
public static getCharacterAttributes(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): CharacterAttributesResponse {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const attributesResult: SeriesCharacterAttributeResult[] = SeriesCharacterRepo.fetchAttributes(characterId, userId, lang);
|
||||
|
||||
const attributes: SeriesAttribute[] = attributesResult.map((attr) => ({
|
||||
id: attr.attr_id,
|
||||
name: System.decryptDataWithUserKey(attr.attribute_value, userKey)
|
||||
}));
|
||||
|
||||
return { attributes };
|
||||
}
|
||||
}
|
||||
154
electron/database/models/SeriesLocation.ts
Normal file
154
electron/database/models/SeriesLocation.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { getUserEncryptionKey } from "../keyManager.js";
|
||||
import System from "../System.js";
|
||||
import SeriesLocationRepo, { SeriesLocationResult, SeriesLocationElementResult, SeriesLocationSubElementResult } from "../repositories/series-location.repo.js";
|
||||
|
||||
export interface SeriesLocationSubElementProps {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface SeriesLocationElementProps {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
subElements: SeriesLocationSubElementProps[];
|
||||
}
|
||||
|
||||
export interface SeriesLocationListProps {
|
||||
id: string;
|
||||
name: string;
|
||||
elements: SeriesLocationElementProps[];
|
||||
}
|
||||
|
||||
export default class SeriesLocation {
|
||||
/**
|
||||
* Retrieves all locations for a series with their elements and sub-elements.
|
||||
* @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 The list of locations
|
||||
*/
|
||||
public static getLocationList(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationListProps[] {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const locationsResult: SeriesLocationResult[] = SeriesLocationRepo.fetchLocations(userId, seriesId, lang);
|
||||
|
||||
return locationsResult.map((loc): SeriesLocationListProps => {
|
||||
const elementsResult: SeriesLocationElementResult[] = SeriesLocationRepo.fetchElements(userId, loc.loc_id, lang);
|
||||
|
||||
const elements: SeriesLocationElementProps[] = elementsResult.map((elem): SeriesLocationElementProps => {
|
||||
const subElementsResult: SeriesLocationSubElementResult[] = SeriesLocationRepo.fetchSubElements(userId, elem.element_id, lang);
|
||||
|
||||
const subElements: SeriesLocationSubElementProps[] = subElementsResult.map((sub): SeriesLocationSubElementProps => ({
|
||||
id: sub.sub_element_id,
|
||||
name: sub.sub_elem_name ? System.decryptDataWithUserKey(sub.sub_elem_name, userKey) : '',
|
||||
description: sub.sub_elem_description ? System.decryptDataWithUserKey(sub.sub_elem_description, userKey) : ''
|
||||
}));
|
||||
|
||||
return {
|
||||
id: elem.element_id,
|
||||
name: elem.element_name ? System.decryptDataWithUserKey(elem.element_name, userKey) : '',
|
||||
description: elem.element_description ? System.decryptDataWithUserKey(elem.element_description, userKey) : '',
|
||||
subElements
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: loc.loc_id,
|
||||
name: loc.loc_name ? System.decryptDataWithUserKey(loc.loc_name, userKey) : '',
|
||||
elements
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new location section to a series.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param seriesId - The unique identifier of the series
|
||||
* @param name - The name of the location
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The new location ID
|
||||
*/
|
||||
public static addLocationSection(userId: string, seriesId: string, name: string, lang: 'fr' | 'en' = 'fr'): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const locationId: string = System.createUniqueId();
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const originalName: string = System.hashElement(name);
|
||||
|
||||
SeriesLocationRepo.insertLocation(locationId, seriesId, userId, encryptedName, originalName, lang);
|
||||
return locationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new element to a location.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param locationId - The unique identifier of the location
|
||||
* @param name - The name of the element
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @param description - The description of the element (optional)
|
||||
* @returns The new element ID
|
||||
*/
|
||||
public static addElement(userId: string, locationId: string, name: string, lang: 'fr' | 'en' = 'fr', description?: string): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const elementId: string = System.createUniqueId();
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const originalName: string = System.hashElement(name);
|
||||
const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null;
|
||||
|
||||
SeriesLocationRepo.insertElement(elementId, locationId, userId, encryptedName, originalName, encryptedDescription, lang);
|
||||
return elementId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new sub-element to an element.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param elementId - The unique identifier of the element
|
||||
* @param name - The name of the sub-element
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @param description - The description of the sub-element (optional)
|
||||
* @returns The new sub-element ID
|
||||
*/
|
||||
public static addSubElement(userId: string, elementId: string, name: string, lang: 'fr' | 'en' = 'fr', description?: string): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const subElementId: string = System.createUniqueId();
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const originalName: string = System.hashElement(name);
|
||||
const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null;
|
||||
|
||||
SeriesLocationRepo.insertSubElement(subElementId, elementId, userId, encryptedName, originalName, encryptedDescription, lang);
|
||||
return subElementId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a location section.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param locationId - The unique identifier of the location
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if successful
|
||||
*/
|
||||
public static deleteLocation(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return SeriesLocationRepo.deleteLocation(userId, locationId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an element.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param elementId - The unique identifier of the element
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if successful
|
||||
*/
|
||||
public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return SeriesLocationRepo.deleteElement(userId, elementId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a sub-element.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param subElementId - The unique identifier of the sub-element
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if successful
|
||||
*/
|
||||
public static deleteSubElement(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return SeriesLocationRepo.deleteSubElement(userId, subElementId, lang);
|
||||
}
|
||||
}
|
||||
211
electron/database/models/SeriesSpell.ts
Normal file
211
electron/database/models/SeriesSpell.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { getUserEncryptionKey } from "../keyManager.js";
|
||||
import System from "../System.js";
|
||||
import SeriesSpellRepo, { SeriesSpellResult, SeriesSpellTagResult } from "../repositories/series-spell.repo.js";
|
||||
|
||||
export interface SeriesSpellTagProps {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string | null;
|
||||
}
|
||||
|
||||
export interface SeriesSpellListProps {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface SeriesSpellListResponse {
|
||||
spells: SeriesSpellListProps[];
|
||||
tags: SeriesSpellTagProps[];
|
||||
}
|
||||
|
||||
export interface SeriesSpellDetailProps {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
appearance: string;
|
||||
tags: string[];
|
||||
powerLevel: string | null;
|
||||
components: string | null;
|
||||
limitations: string | null;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
export default class SeriesSpell {
|
||||
/**
|
||||
* Retrieves all spells and tags for a series.
|
||||
* @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 The list of spells and tags
|
||||
*/
|
||||
public static getSpellList(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellListResponse {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const spellsResult: SeriesSpellResult[] = SeriesSpellRepo.fetchSpells(userId, seriesId, lang);
|
||||
const tagsResult: SeriesSpellTagResult[] = SeriesSpellRepo.fetchTags(userId, seriesId, lang);
|
||||
|
||||
const spells: SeriesSpellListProps[] = spellsResult.map((spell): SeriesSpellListProps => ({
|
||||
id: spell.spell_id,
|
||||
name: spell.name ? System.decryptDataWithUserKey(spell.name, userKey) : '',
|
||||
description: spell.description ? System.decryptDataWithUserKey(spell.description, userKey) : '',
|
||||
tags: spell.tags ? JSON.parse(System.decryptDataWithUserKey(spell.tags, userKey)) : []
|
||||
}));
|
||||
|
||||
const tags: SeriesSpellTagProps[] = tagsResult.map((tag): SeriesSpellTagProps => ({
|
||||
id: tag.tag_id,
|
||||
name: tag.name ? System.decryptDataWithUserKey(tag.name, userKey) : '',
|
||||
color: tag.color
|
||||
}));
|
||||
|
||||
return { spells, tags };
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the details of a specific spell.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The spell details
|
||||
*/
|
||||
public static getSpellDetail(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellDetailProps {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const spell: SeriesSpellResult | null = SeriesSpellRepo.fetchSpellById(userId, spellId, lang);
|
||||
|
||||
if (!spell) {
|
||||
throw new Error(lang === 'fr' ? 'Sort non trouvé.' : 'Spell not found.');
|
||||
}
|
||||
|
||||
return {
|
||||
id: spell.spell_id,
|
||||
name: spell.name ? System.decryptDataWithUserKey(spell.name, userKey) : '',
|
||||
description: spell.description ? System.decryptDataWithUserKey(spell.description, userKey) : '',
|
||||
appearance: spell.appearance ? System.decryptDataWithUserKey(spell.appearance, userKey) : '',
|
||||
tags: spell.tags ? JSON.parse(System.decryptDataWithUserKey(spell.tags, userKey)) : [],
|
||||
powerLevel: spell.power_level ? System.decryptDataWithUserKey(spell.power_level, userKey) : null,
|
||||
components: spell.components ? System.decryptDataWithUserKey(spell.components, userKey) : null,
|
||||
limitations: spell.limitations ? System.decryptDataWithUserKey(spell.limitations, userKey) : null,
|
||||
notes: spell.notes ? System.decryptDataWithUserKey(spell.notes, userKey) : null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new spell to a series.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param seriesId - The unique identifier of the series
|
||||
* @param name - The spell name
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @param description - The spell description
|
||||
* @param appearance - The spell appearance
|
||||
* @param tags - The spell tags
|
||||
* @param powerLevel - The spell power level
|
||||
* @param components - The spell components
|
||||
* @param limitations - The spell limitations
|
||||
* @param notes - The spell notes
|
||||
* @returns The new spell ID
|
||||
*/
|
||||
public static addSpell(userId: string, seriesId: string, name: string, lang: 'fr' | 'en' = 'fr', description?: string | null, appearance?: string | null, tags?: string[], powerLevel?: string | null, components?: string | null, limitations?: string | null, notes?: string | null): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const spellId: string = System.createUniqueId();
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const nameHash: string = System.hashElement(name);
|
||||
const encryptedDescription: string = description ? System.encryptDataWithUserKey(description, userKey) : '';
|
||||
const encryptedAppearance: string = appearance ? System.encryptDataWithUserKey(appearance, userKey) : '';
|
||||
const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(tags || []), userKey);
|
||||
const encryptedPowerLevel: string | null = powerLevel ? System.encryptDataWithUserKey(powerLevel, userKey) : null;
|
||||
const encryptedComponents: string | null = components ? System.encryptDataWithUserKey(components, userKey) : null;
|
||||
const encryptedLimitations: string | null = limitations ? System.encryptDataWithUserKey(limitations, userKey) : null;
|
||||
const encryptedNotes: string | null = notes ? System.encryptDataWithUserKey(notes, userKey) : null;
|
||||
|
||||
SeriesSpellRepo.insertSpell(spellId, seriesId, userId, encryptedName, nameHash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, lang);
|
||||
return spellId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing spell.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param name - The spell name
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @param description - The spell description
|
||||
* @param appearance - The spell appearance
|
||||
* @param tags - The spell tags
|
||||
* @param powerLevel - The spell power level
|
||||
* @param components - The spell components
|
||||
* @param limitations - The spell limitations
|
||||
* @param notes - The spell notes
|
||||
* @returns True if successful
|
||||
*/
|
||||
public static updateSpell(userId: string, spellId: string, name: string, lang: 'fr' | 'en' = 'fr', description?: string | null, appearance?: string | null, tags?: string[], powerLevel?: string | null, components?: string | null, limitations?: string | null, notes?: string | null): boolean {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const nameHash: string = System.hashElement(name);
|
||||
const encryptedDescription: string = description ? System.encryptDataWithUserKey(description, userKey) : '';
|
||||
const encryptedAppearance: string = appearance ? System.encryptDataWithUserKey(appearance, userKey) : '';
|
||||
const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(tags || []), userKey);
|
||||
const encryptedPowerLevel: string | null = powerLevel ? System.encryptDataWithUserKey(powerLevel, userKey) : null;
|
||||
const encryptedComponents: string | null = components ? System.encryptDataWithUserKey(components, userKey) : null;
|
||||
const encryptedLimitations: string | null = limitations ? System.encryptDataWithUserKey(limitations, userKey) : null;
|
||||
const encryptedNotes: string | null = notes ? System.encryptDataWithUserKey(notes, userKey) : null;
|
||||
|
||||
return SeriesSpellRepo.updateSpell(userId, spellId, encryptedName, nameHash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a spell.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if successful
|
||||
*/
|
||||
public static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return SeriesSpellRepo.deleteSpell(userId, spellId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new tag to a series.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param seriesId - The unique identifier of the series
|
||||
* @param name - The name of the tag
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @param color - The color of the tag (optional)
|
||||
* @returns The new tag ID
|
||||
*/
|
||||
public static addTag(userId: string, seriesId: string, name: string, lang: 'fr' | 'en' = 'fr', color?: string | null): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const tagId: string = System.createUniqueId();
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const hashedName: string = System.hashElement(name);
|
||||
|
||||
SeriesSpellRepo.insertTag(tagId, seriesId, userId, encryptedName, hashedName, color || null, lang);
|
||||
return tagId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing tag.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param tagId - The unique identifier of the tag
|
||||
* @param name - The new name of the tag
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @param color - The new color of the tag (optional)
|
||||
* @returns True if successful
|
||||
*/
|
||||
public static updateTag(userId: string, tagId: string, name: string, lang: 'fr' | 'en' = 'fr', color?: string | null): boolean {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const hashedName: string = System.hashElement(name);
|
||||
|
||||
return SeriesSpellRepo.updateTag(userId, tagId, encryptedName, hashedName, color || null, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a tag.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param tagId - The unique identifier of the tag
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if successful
|
||||
*/
|
||||
public static deleteTag(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return SeriesSpellRepo.deleteTag(userId, tagId, lang);
|
||||
}
|
||||
}
|
||||
199
electron/database/models/SeriesSync.ts
Normal file
199
electron/database/models/SeriesSync.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
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<string, string> = {
|
||||
'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<string, string> = {
|
||||
'name': 'name',
|
||||
'history': 'history',
|
||||
'politics': 'politics',
|
||||
'economy': 'economy',
|
||||
'religion': 'religion',
|
||||
'languages': 'languages'
|
||||
};
|
||||
|
||||
const locationFieldMap: Record<string, string> = {
|
||||
'name': 'name',
|
||||
'loc_name': 'loc_name'
|
||||
};
|
||||
|
||||
const spellFieldMap: Record<string, string> = {
|
||||
'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;
|
||||
}
|
||||
}
|
||||
190
electron/database/models/SeriesWorld.ts
Normal file
190
electron/database/models/SeriesWorld.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { getUserEncryptionKey } from "../keyManager.js";
|
||||
import System from "../System.js";
|
||||
import SeriesWorldRepo, { SeriesWorldResult } from "../repositories/series-world.repo.js";
|
||||
|
||||
export interface SeriesWorldElementProps {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface SeriesWorldListProps {
|
||||
id: string;
|
||||
name: string;
|
||||
history: string;
|
||||
politics: string;
|
||||
economy: string;
|
||||
religion: string;
|
||||
languages: string;
|
||||
laws: SeriesWorldElementProps[];
|
||||
biomes: SeriesWorldElementProps[];
|
||||
issues: SeriesWorldElementProps[];
|
||||
customs: SeriesWorldElementProps[];
|
||||
kingdoms: SeriesWorldElementProps[];
|
||||
climate: SeriesWorldElementProps[];
|
||||
resources: SeriesWorldElementProps[];
|
||||
wildlife: SeriesWorldElementProps[];
|
||||
arts: SeriesWorldElementProps[];
|
||||
ethnicGroups: SeriesWorldElementProps[];
|
||||
socialClasses: SeriesWorldElementProps[];
|
||||
importantCharacters: SeriesWorldElementProps[];
|
||||
}
|
||||
|
||||
export interface SeriesWorldUpdateProps {
|
||||
name: string;
|
||||
history?: string;
|
||||
politics?: string;
|
||||
economy?: string;
|
||||
religion?: string;
|
||||
languages?: string;
|
||||
}
|
||||
|
||||
const ELEMENT_TYPE_MAP: Record<number, keyof SeriesWorldListProps> = {
|
||||
0: 'laws',
|
||||
1: 'biomes',
|
||||
2: 'issues',
|
||||
3: 'customs',
|
||||
4: 'kingdoms',
|
||||
5: 'climate',
|
||||
6: 'resources',
|
||||
7: 'wildlife',
|
||||
8: 'arts',
|
||||
9: 'ethnicGroups',
|
||||
10: 'socialClasses',
|
||||
11: 'importantCharacters'
|
||||
};
|
||||
|
||||
export default class SeriesWorld {
|
||||
/**
|
||||
* Retrieves all worlds and their elements for a series.
|
||||
* @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 The list of worlds
|
||||
*/
|
||||
public static getWorldList(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldListProps[] {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const worldsResult: SeriesWorldResult[] = SeriesWorldRepo.fetchWorlds(userId, seriesId, lang);
|
||||
|
||||
const worldsMap: Map<string, SeriesWorldListProps> = new Map();
|
||||
|
||||
for (const row of worldsResult) {
|
||||
if (!worldsMap.has(row.world_id)) {
|
||||
worldsMap.set(row.world_id, {
|
||||
id: row.world_id,
|
||||
name: row.world_name ? System.decryptDataWithUserKey(row.world_name, userKey) : '',
|
||||
history: row.history ? System.decryptDataWithUserKey(row.history, userKey) : '',
|
||||
politics: row.politics ? System.decryptDataWithUserKey(row.politics, userKey) : '',
|
||||
economy: row.economy ? System.decryptDataWithUserKey(row.economy, userKey) : '',
|
||||
religion: row.religion ? System.decryptDataWithUserKey(row.religion, userKey) : '',
|
||||
languages: row.languages ? System.decryptDataWithUserKey(row.languages, userKey) : '',
|
||||
laws: [],
|
||||
biomes: [],
|
||||
issues: [],
|
||||
customs: [],
|
||||
kingdoms: [],
|
||||
climate: [],
|
||||
resources: [],
|
||||
wildlife: [],
|
||||
arts: [],
|
||||
ethnicGroups: [],
|
||||
socialClasses: [],
|
||||
importantCharacters: []
|
||||
});
|
||||
}
|
||||
|
||||
if (row.element_id) {
|
||||
const world = worldsMap.get(row.world_id)!;
|
||||
const element: SeriesWorldElementProps = {
|
||||
id: row.element_id,
|
||||
name: row.element_name ? System.decryptDataWithUserKey(row.element_name, userKey) : '',
|
||||
description: row.element_description ? System.decryptDataWithUserKey(row.element_description, userKey) : ''
|
||||
};
|
||||
|
||||
const key = ELEMENT_TYPE_MAP[row.element_type];
|
||||
if (key && Array.isArray(world[key])) {
|
||||
(world[key] as SeriesWorldElementProps[]).push(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(worldsMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new world to a series.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param seriesId - The unique identifier of the series
|
||||
* @param name - The name of the world
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The new world ID
|
||||
*/
|
||||
public static addWorld(userId: string, seriesId: string, name: string, lang: 'fr' | 'en' = 'fr'): string {
|
||||
const hashedName: string = System.hashElement(name);
|
||||
|
||||
const exists: boolean = SeriesWorldRepo.checkWorldExist(userId, seriesId, hashedName, lang);
|
||||
if (exists) {
|
||||
throw new Error(lang === 'fr' ? 'Un monde avec ce nom existe déjà.' : 'A world with this name already exists.');
|
||||
}
|
||||
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const worldId: string = System.createUniqueId();
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
|
||||
SeriesWorldRepo.insertNewWorld(worldId, userId, seriesId, encryptedName, hashedName, lang);
|
||||
return worldId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a world's information.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param worldId - The unique identifier of the world
|
||||
* @param world - The updated world data
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if successful
|
||||
*/
|
||||
public static updateWorld(userId: string, worldId: string, world: SeriesWorldUpdateProps, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(world.name, userKey);
|
||||
const hashedName: string = System.hashElement(world.name);
|
||||
const encryptedHistory: string | null = world.history ? System.encryptDataWithUserKey(world.history, userKey) : null;
|
||||
const encryptedPolitics: string | null = world.politics ? System.encryptDataWithUserKey(world.politics, userKey) : null;
|
||||
const encryptedEconomy: string | null = world.economy ? System.encryptDataWithUserKey(world.economy, userKey) : null;
|
||||
const encryptedReligion: string | null = world.religion ? System.encryptDataWithUserKey(world.religion, userKey) : null;
|
||||
const encryptedLanguages: string | null = world.languages ? System.encryptDataWithUserKey(world.languages, userKey) : null;
|
||||
|
||||
return SeriesWorldRepo.updateWorld(userId, worldId, encryptedName, hashedName, encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new element to a world.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param worldId - The unique identifier of the world
|
||||
* @param elementType - The type of element (0-11)
|
||||
* @param name - The name of the element
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @param description - The description of the element (optional)
|
||||
* @returns The new element ID
|
||||
*/
|
||||
public static addElement(userId: string, worldId: string, elementType: number, name: string, lang: 'fr' | 'en' = 'fr', description?: string): string {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const elementId: string = System.createUniqueId();
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const originalName: string = System.hashElement(name);
|
||||
const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null;
|
||||
|
||||
SeriesWorldRepo.insertElement(elementId, worldId, userId, elementType, encryptedName, originalName, encryptedDescription, lang);
|
||||
return elementId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes an element from a world.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param elementId - The unique identifier of the element
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if successful
|
||||
*/
|
||||
public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return SeriesWorldRepo.deleteElement(userId, elementId, lang);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ export interface SpellProps {
|
||||
components: string | null;
|
||||
limitations: string | null;
|
||||
notes: string | null;
|
||||
seriesSpellId: string | null;
|
||||
}
|
||||
|
||||
export interface SpellListItem {
|
||||
@@ -27,6 +28,7 @@ export interface SpellListItem {
|
||||
name: string;
|
||||
description: string;
|
||||
tags: SpellTagProps[];
|
||||
seriesSpellId?: string | null;
|
||||
}
|
||||
|
||||
export interface SpellListResponse {
|
||||
@@ -193,6 +195,7 @@ export default class Spell {
|
||||
name: decryptedName,
|
||||
description: truncatedDescription,
|
||||
tags: resolvedTags,
|
||||
seriesSpellId: spell.series_spell_id || null,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -240,6 +243,7 @@ export default class Spell {
|
||||
components: spell.components ? System.decryptDataWithUserKey(spell.components, userKey) : null,
|
||||
limitations: spell.limitations ? System.decryptDataWithUserKey(spell.limitations, userKey) : null,
|
||||
notes: spell.notes ? System.decryptDataWithUserKey(spell.notes, userKey) : null,
|
||||
seriesSpellId: spell.series_spell_id || null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -259,7 +263,7 @@ export default class Spell {
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The created spell props
|
||||
*/
|
||||
static addSpell(userId: string, bookId: string, name: string, description: string, appearance: string, tags: string[], powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, existingSpellId?: string, lang: 'fr' | 'en' = 'fr'): SpellProps {
|
||||
static addSpell(userId: string, bookId: string, name: string, description: string, appearance: string, tags: string[], powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, existingSpellId?: string, lang: 'fr' | 'en' = 'fr', seriesSpellId: string | null = null): SpellProps {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const spellId: string = existingSpellId || System.createUniqueId();
|
||||
|
||||
@@ -287,6 +291,7 @@ export default class Spell {
|
||||
encryptedLimitations,
|
||||
encryptedNotes,
|
||||
lang,
|
||||
seriesSpellId,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -299,6 +304,7 @@ export default class Spell {
|
||||
components,
|
||||
limitations,
|
||||
notes,
|
||||
seriesSpellId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -317,7 +323,7 @@ export default class Spell {
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the update was successful
|
||||
*/
|
||||
static updateSpell(userId: string, spellId: string, name: string, description: string, appearance: string, tags: string[], powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
static updateSpell(userId: string, spellId: string, name: string, description: string, appearance: string, tags: string[], powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr', seriesSpellId: string | null = null): boolean {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
@@ -343,6 +349,7 @@ export default class Spell {
|
||||
encryptedLimitations,
|
||||
encryptedNotes,
|
||||
lang,
|
||||
seriesSpellId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1068,7 +1068,11 @@ export default class Sync {
|
||||
|
||||
const bookToolsQuery: SyncedBookToolsResult | null = BookRepo.fetchSyncedBookTools(userId, currentBookId, lang);
|
||||
const bookTools: SyncedBookTools | null = bookToolsQuery ? {
|
||||
lastUpdate: bookToolsQuery.last_update
|
||||
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
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface WorldProps {
|
||||
ethnicGroups: WorldElement[];
|
||||
socialClasses: WorldElement[];
|
||||
importantCharacters: WorldElement[];
|
||||
seriesWorldId?: string | null;
|
||||
}
|
||||
|
||||
export interface WorldListResponse {
|
||||
@@ -97,7 +98,7 @@ export default class World {
|
||||
* @returns The unique identifier of the newly created world
|
||||
* @throws Error if a world with the same name already exists for this book
|
||||
*/
|
||||
public static addNewWorld(userId: string, bookId: string, worldName: string, lang: 'fr' | 'en' = 'fr', existingWorldId?: string): string {
|
||||
public static addNewWorld(userId: string, bookId: string, worldName: string, lang: 'fr' | 'en' = 'fr', existingWorldId?: string, seriesWorldId: string | null = null): string {
|
||||
const userEncryptionKey: string = getUserEncryptionKey(userId);
|
||||
const hashedWorldName: string = System.hashElement(worldName);
|
||||
if (!existingWorldId && WorldRepository.checkWorldExist(userId, bookId, hashedWorldName, lang)) {
|
||||
@@ -105,7 +106,7 @@ export default class World {
|
||||
}
|
||||
const encryptedWorldName: string = System.encryptDataWithUserKey(worldName, userEncryptionKey);
|
||||
const worldId: string = existingWorldId || System.createUniqueId();
|
||||
return WorldRepository.insertNewWorld(worldId, userId, bookId, encryptedWorldName, hashedWorldName, lang);
|
||||
return WorldRepository.insertNewWorld(worldId, userId, bookId, encryptedWorldName, hashedWorldName, lang, seriesWorldId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,6 +148,7 @@ export default class World {
|
||||
ethnicGroups: [],
|
||||
socialClasses: [],
|
||||
importantCharacters: [],
|
||||
seriesWorldId: queryRow.series_world_id || null,
|
||||
};
|
||||
|
||||
worlds.push(newWorld);
|
||||
@@ -228,7 +230,7 @@ export default class World {
|
||||
}));
|
||||
});
|
||||
|
||||
WorldRepository.updateWorld(userId, world.id, encryptedName, System.hashElement(world.name), encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, System.timeStampInSeconds(), lang);
|
||||
WorldRepository.updateWorld(userId, world.id, encryptedName, System.hashElement(world.name), encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, System.timeStampInSeconds(), lang, world.seriesWorldId || null);
|
||||
return WorldRepository.updateWorldElements(userId, elementsToUpdate, lang);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user