Add comprehensive spell management functionality

- Introduced spell management with creation, editing, deletion, and tagging capabilities.
- Added `Spell`, `SpellList`, `SpellTagManager` models with corresponding IPC handlers for data operations.
- Implemented `SpellList` and `SpellTagChip` components for UI interactions with spells and tags.
- Localized spell-related strings for English (e.g., error messages, tooltips, and prompts).
- Enhanced database models and repositories with encryption and decryption workflows for secure data handling.
- Updated API to include filtering, searching, and tag-based spell management options.
This commit is contained in:
natreex
2026-01-19 21:38:38 -05:00
parent c62a7eb0f7
commit fd09a5531c
14 changed files with 2717 additions and 1 deletions

View File

@@ -0,0 +1,359 @@
import SpellRepo, { SpellResult } from '../repositories/spell.repo.js';
import SpellTagRepo, { SpellTagResult } from '../repositories/spelltag.repo.js';
import BookRepo, { BookToolsTable } from '../repositories/book.repository.js';
import System from '../System.js';
import { getUserEncryptionKey } from '../keyManager.js';
export interface SpellTagProps {
id: string;
name: string;
color: string | null;
}
export interface SpellProps {
id: string;
name: string;
description: string;
appearance: string;
tags: string[];
powerLevel: string | null;
components: string | null;
limitations: string | null;
notes: string | null;
}
export interface SpellListItem {
id: string;
name: string;
description: string;
tags: SpellTagProps[];
}
export interface SpellListResponse {
enabled: boolean;
spells: SpellListItem[];
tags: SpellTagProps[];
}
export interface SyncedSpell {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedSpellTag {
id: string;
name: string;
lastUpdate: number;
}
export default class Spell {
/**
* Retrieves all spell tags for a specific book.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param lang - The language for error messages ('fr' or 'en')
* @returns An array of spell tag props
*/
static getSpellTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellTagProps[] {
const userKey: string = getUserEncryptionKey(userId);
const spellTags: SpellTagResult[] = SpellTagRepo.fetchSpellTags(userId, bookId, lang);
return spellTags.map((tag: SpellTagResult): SpellTagProps => ({
id: tag.tag_id,
name: System.decryptDataWithUserKey(tag.name, userKey),
color: tag.color,
}));
}
/**
* Adds a new spell tag to a book.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param name - The name of the tag
* @param color - The optional color hex code
* @param existingTagId - Optional existing tag ID for sync
* @param lang - The language for error messages ('fr' or 'en')
* @returns The created spell tag props
*/
static addSpellTag(userId: string, bookId: string, name: string, color: string | null, existingTagId?: string, lang: 'fr' | 'en' = 'fr'): SpellTagProps {
const userKey: string = getUserEncryptionKey(userId);
const tagId: string = existingTagId || System.createUniqueId();
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
const nameHash: string = System.hashElement(name);
SpellTagRepo.insertSpellTag(tagId, bookId, userId, encryptedName, nameHash, color, lang);
return {
id: tagId,
name: name,
color: color,
};
}
/**
* Updates an existing spell 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 color - The new optional color hex code
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the update was successful
*/
static updateSpellTag(userId: string, tagId: string, name: string, color: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
const userKey: string = getUserEncryptionKey(userId);
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
const nameHash: string = System.hashElement(name);
return SpellTagRepo.updateSpellTag(userId, tagId, encryptedName, nameHash, color, lang);
}
/**
* Deletes a spell tag and removes its references from all spells in the book.
* @param userId - The unique identifier of the user
* @param tagId - The unique identifier of the tag to delete
* @param bookId - The unique identifier of the book
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the deletion was successful
*/
static deleteSpellTag(userId: string, tagId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean {
const userKey: string = getUserEncryptionKey(userId);
const spells: SpellResult[] = SpellRepo.fetchSpells(userId, bookId, lang);
for (const spell of spells) {
const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey);
let tagsArray: string[] = [];
try {
tagsArray = JSON.parse(decryptedTags) as string[];
} catch {
tagsArray = [];
}
if (tagsArray.includes(tagId)) {
const updatedTags: string[] = tagsArray.filter((t: string): boolean => t !== tagId);
const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(updatedTags), userKey);
SpellRepo.updateSpellTags(userId, spell.spell_id, encryptedTags, lang);
}
}
// Then delete the tag
return SpellTagRepo.deleteSpellTag(userId, tagId, lang);
}
/**
* Retrieves the spell list with tags for a specific book.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param lang - The language for error messages ('fr' or 'en')
* @returns The spell list response with enabled status, spells, and tags
*/
static getSpellList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellListResponse {
const userKey: string = getUserEncryptionKey(userId);
const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang);
const enabled: boolean = bookTools ? bookTools.spells_enabled === 1 : false;
const spellTags: SpellTagResult[] = SpellTagRepo.fetchSpellTags(userId, bookId, lang);
const tags: SpellTagProps[] = spellTags.map((tag: SpellTagResult): SpellTagProps => ({
id: tag.tag_id,
name: System.decryptDataWithUserKey(tag.name, userKey),
color: tag.color,
}));
const tagMap: Map<string, SpellTagProps> = new Map();
for (const tag of tags) {
tagMap.set(tag.id, tag);
}
const spellResults: SpellResult[] = SpellRepo.fetchSpells(userId, bookId, lang);
const spells: SpellListItem[] = spellResults.map((spell: SpellResult): SpellListItem => {
const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey);
const decryptedDescription: string = System.decryptDataWithUserKey(spell.description, userKey);
const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey);
let tagIds: string[];
try {
tagIds = JSON.parse(decryptedTags) as string[];
} catch {
tagIds = [];
}
const resolvedTags: SpellTagProps[] = tagIds
.map((tagId: string): SpellTagProps | undefined => tagMap.get(tagId))
.filter((tag: SpellTagProps | undefined): tag is SpellTagProps => tag !== undefined);
const truncatedDescription: string = decryptedDescription.length > 150
? decryptedDescription.substring(0, 150) + '...'
: decryptedDescription;
return {
id: spell.spell_id,
name: decryptedName,
description: truncatedDescription,
tags: resolvedTags,
};
});
return {
enabled,
spells,
tags,
};
}
/**
* Retrieves the full 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 props with all details
*/
static getSpellDetail(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SpellProps {
const userKey: string = getUserEncryptionKey(userId);
const spell: SpellResult | null = SpellRepo.fetchSpellById(userId, spellId, lang);
if (!spell) {
throw new Error(lang === 'fr' ? 'Sort non trouvé.' : 'Spell not found.');
}
const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey);
const decryptedDescription: string = System.decryptDataWithUserKey(spell.description, userKey);
const decryptedAppearance: string = System.decryptDataWithUserKey(spell.appearance, userKey);
const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey);
let tagIds: string[];
try {
tagIds = JSON.parse(decryptedTags) as string[];
} catch {
tagIds = [];
}
return {
id: spell.spell_id,
name: decryptedName,
description: decryptedDescription,
appearance: decryptedAppearance,
tags: tagIds,
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 book.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param name - The name of the spell
* @param description - The description of the spell
* @param appearance - The appearance of the spell
* @param tags - The tag IDs array
* @param powerLevel - The optional power level
* @param components - The optional components
* @param limitations - The optional limitations
* @param notes - The optional notes
* @param existingSpellId - Optional existing spell ID for sync
* @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 {
const userKey: string = getUserEncryptionKey(userId);
const spellId: string = existingSpellId || System.createUniqueId();
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
const nameHash: string = System.hashElement(name);
const encryptedDescription: string = System.encryptDataWithUserKey(description, userKey);
const encryptedAppearance: string = 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;
SpellRepo.insertSpell(
spellId,
bookId,
userId,
encryptedName,
nameHash,
encryptedDescription,
encryptedAppearance,
encryptedTags,
encryptedPowerLevel,
encryptedComponents,
encryptedLimitations,
encryptedNotes,
lang,
);
return {
id: spellId,
name,
description,
appearance,
tags,
powerLevel,
components,
limitations,
notes,
};
}
/**
* Updates an existing spell.
* @param userId - The unique identifier of the user
* @param spellId - The unique identifier of the spell
* @param name - The name of the spell
* @param description - The description of the spell
* @param appearance - The appearance of the spell
* @param tags - The tag IDs array
* @param powerLevel - The optional power level
* @param components - The optional components
* @param limitations - The optional limitations
* @param notes - The optional notes
* @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 {
const userKey: string = getUserEncryptionKey(userId);
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
const nameHash: string = System.hashElement(name);
const encryptedDescription: string = System.encryptDataWithUserKey(description, userKey);
const encryptedAppearance: string = 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 SpellRepo.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 the deletion was successful
*/
static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return SpellRepo.deleteSpell(userId, spellId, lang);
}
}

View File

@@ -0,0 +1,368 @@
import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
import System from '../System.js';
export interface SpellResult extends Record<string, SQLiteValue> {
spell_id: string;
book_id: string;
name: string;
description: string;
appearance: string;
tags: string;
power_level: string | null;
components: string | null;
limitations: string | null;
notes: string | null;
}
export interface BookSpellsTable extends Record<string, SQLiteValue> {
spell_id: string;
book_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 SyncedSpellResult extends Record<string, SQLiteValue> {
spell_id: string;
book_id: string;
name: string;
last_update: number;
}
export default class SpellRepo {
/**
* Fetches all spells for a specific book owned by the user.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param lang - The language for error messages ('fr' or 'en')
* @returns An array of spell results
*/
static fetchSpells(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes FROM book_spells WHERE user_id=? AND book_id=?';
const params: SQLiteValue[] = [userId, bookId];
return db.all(query, params) as SpellResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts.` : `Unable to retrieve spells.`);
}
}
/**
* Fetches a single spell by its ID.
* @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 result or null if not found
*/
static fetchSpellById(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SpellResult | null {
try {
const db: Database = System.getDb();
const query: string = 'SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes FROM book_spells WHERE user_id=? AND spell_id=?';
const params: SQLiteValue[] = [userId, spellId];
const spells: SpellResult[] = db.all(query, params) as SpellResult[];
return spells.length > 0 ? spells[0] : null;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible de récupérer le sort.` : `Unable to retrieve spell.`);
}
}
/**
* Inserts a new spell.
* @param spellId - The unique identifier for the new spell
* @param bookId - The unique identifier of the book
* @param userId - The unique identifier of the user
* @param name - The encrypted name
* @param nameHash - The hashed name for duplicate detection
* @param description - The encrypted description
* @param appearance - The encrypted appearance
* @param tags - The encrypted JSON tags array
* @param powerLevel - The encrypted power level (nullable)
* @param components - The encrypted components (nullable)
* @param limitations - The encrypted limitations (nullable)
* @param notes - The encrypted notes (nullable)
* @param lang - The language for error messages ('fr' or 'en')
* @returns The spell ID if successful
*/
static insertSpell(spellId: string, bookId: string, userId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): string {
try {
const db: Database = System.getDb();
const query: string = 'INSERT INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)';
const params: SQLiteValue[] = [spellId, bookId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds()];
const result: RunResult = db.run(query, params);
if (!result || result.changes === 0) {
throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du sort.` : `Error adding spell.`);
}
return spellId;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible d'ajouter le sort.` : `Unable to add spell.`);
}
}
/**
* Updates an existing spell.
* @param userId - The unique identifier of the user
* @param spellId - The unique identifier of the spell
* @param name - The encrypted name
* @param nameHash - The hashed name
* @param description - The encrypted description
* @param appearance - The encrypted appearance
* @param tags - The encrypted JSON tags array
* @param powerLevel - The encrypted power level (nullable)
* @param components - The encrypted components (nullable)
* @param limitations - The encrypted limitations (nullable)
* @param notes - The encrypted notes (nullable)
* @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, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE book_spells SET name=?, name_hash=?, description=?, appearance=?, tags=?, power_level=?, components=?, limitations=?, notes=?, last_update=? WHERE spell_id=? AND user_id=?';
const params: SQLiteValue[] = [name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds(), spellId, userId];
const result: RunResult = db.run(query, params);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort.` : `Unable to update spell.`);
}
}
/**
* 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 the deletion was successful
*/
static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'DELETE FROM book_spells WHERE spell_id=? AND user_id=?';
const params: SQLiteValue[] = [spellId, userId];
const result: RunResult = db.run(query, params);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible de supprimer le sort.` : `Unable to delete spell.`);
}
}
/**
* Updates the tags field of a spell.
* @param userId - The unique identifier of the user
* @param spellId - The unique identifier of the spell
* @param tags - The new encrypted JSON tags array
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the update was successful
*/
static updateSpellTags(userId: string, spellId: string, tags: string, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE book_spells SET tags=?, last_update=? WHERE spell_id=? AND user_id=?';
const params: SQLiteValue[] = [tags, System.timeStampInSeconds(), spellId, userId];
const result: RunResult = db.run(query, params);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible de mettre à jour les tags du sort.` : `Unable to update spell tags.`);
}
}
/**
* Fetches all spells for a book with full table data for sync.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param lang - The language for error messages ('fr' or 'en')
* @returns An array of book spells table records
*/
static fetchBookSpellsTable(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): BookSpellsTable[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM book_spells WHERE user_id=? AND book_id=?';
const params: SQLiteValue[] = [userId, bookId];
return db.all(query, params) as BookSpellsTable[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts.` : `Unable to retrieve spells.`);
}
}
/**
* Fetches a complete spell record by its ID.
* @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 table record or null
*/
static fetchSpellTableById(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): BookSpellsTable | null {
try {
const db: Database = System.getDb();
const query: string = 'SELECT spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM book_spells WHERE user_id=? AND spell_id=?';
const params: SQLiteValue[] = [userId, spellId];
const spells: BookSpellsTable[] = db.all(query, params) as BookSpellsTable[];
return spells.length > 0 ? spells[0] : null;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible de récupérer le sort.` : `Unable to retrieve spell.`);
}
}
/**
* Fetches all synced spells for a user.
* @param userId - The unique identifier of the user
* @param lang - The language for error messages ('fr' or 'en')
* @returns An array of synced spell results
*/
static fetchSyncedSpells(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSpellResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT spell_id, book_id, name, last_update FROM book_spells WHERE user_id=?';
const params: SQLiteValue[] = [userId];
return db.all(query, params) as SyncedSpellResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts synchronisés.` : `Unable to retrieve synced spells.`);
}
}
/**
* Inserts or updates a spell from synchronization data.
* @param spellId - The unique identifier for the spell
* @param bookId - The unique identifier of the book
* @param userId - The unique identifier of the user
* @param name - The encrypted name
* @param nameHash - The hashed name
* @param description - The encrypted description
* @param appearance - The encrypted appearance
* @param tags - The encrypted JSON tags array
* @param powerLevel - The encrypted power level (nullable)
* @param components - The encrypted components (nullable)
* @param limitations - The encrypted limitations (nullable)
* @param notes - The encrypted notes (nullable)
* @param lastUpdate - The timestamp of the last update
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the insertion was successful
*/
static insertSyncSpell(spellId: string, bookId: string, userId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'INSERT OR REPLACE INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)';
const params: SQLiteValue[] = [spellId, bookId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate];
const result: RunResult = db.run(query, params);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible d'insérer le sort.` : `Unable to insert spell.`);
}
}
/**
* Checks if a spell exists.
* @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 the spell exists
*/
static isSpellExist(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'SELECT 1 FROM book_spells WHERE spell_id=? AND user_id=?';
const params: SQLiteValue[] = [spellId, userId];
const existenceCheck = db.all(query, params);
return existenceCheck.length > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du sort.` : `Unable to check spell existence.`);
}
}
/**
* Updates a spell with timestamp for sync.
* @param userId - The unique identifier of the user
* @param spellId - The unique identifier of the spell
* @param name - The encrypted name
* @param nameHash - The hashed name
* @param description - The encrypted description
* @param appearance - The encrypted appearance
* @param tags - The encrypted JSON tags array
* @param powerLevel - The encrypted power level (nullable)
* @param components - The encrypted components (nullable)
* @param limitations - The encrypted limitations (nullable)
* @param notes - The encrypted notes (nullable)
* @param lastUpdate - The timestamp of the last update
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the update was successful
*/
static updateSyncSpell(userId: string, spellId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE book_spells SET name=?, name_hash=?, description=?, appearance=?, tags=?, power_level=?, components=?, limitations=?, notes=?, last_update=? WHERE spell_id=? AND user_id=?';
const params: SQLiteValue[] = [name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate, spellId, userId];
const result: RunResult = db.run(query, params);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort.` : `Unable to update spell.`);
}
}
}

View File

@@ -0,0 +1,292 @@
import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
import System from '../System.js';
export interface SpellTagResult extends Record<string, SQLiteValue> {
tag_id: string;
book_id: string;
name: string;
color: string | null;
}
export interface BookSpellTagsTable extends Record<string, SQLiteValue> {
tag_id: string;
book_id: string;
user_id: string;
name: string;
name_hash: string;
color: string | null;
last_update: number;
}
export interface SyncedSpellTagResult extends Record<string, SQLiteValue> {
tag_id: string;
book_id: string;
name: string;
last_update: number;
}
export default class SpellTagRepo {
/**
* Fetches all spell tags for a specific book owned by the user.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param lang - The language for error messages ('fr' or 'en')
* @returns An array of spell tag results
*/
static fetchSpellTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellTagResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT tag_id, book_id, name, color FROM book_spell_tags WHERE user_id=? AND book_id=?';
const params: SQLiteValue[] = [userId, bookId];
return db.all(query, params) as SpellTagResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sorts.` : `Unable to retrieve spell tags.`);
} else {
console.error('[SpellTagRepo] An unknown error occurred.');
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
}
}
}
/**
* Inserts a new spell tag.
* @param tagId - The unique identifier for the new tag
* @param bookId - The unique identifier of the book
* @param userId - The unique identifier of the user
* @param name - The encrypted name of the tag
* @param nameHash - The hashed name for duplicate detection
* @param color - The optional color hex code
* @param lang - The language for error messages ('fr' or 'en')
* @returns The tag ID if successful
*/
static insertSpellTag(tagId: string, bookId: string, userId: string, name: string, nameHash: string, color: string | null, lang: 'fr' | 'en' = 'fr'): string {
try {
const db: Database = System.getDb();
const query: string = 'INSERT INTO book_spell_tags (tag_id, book_id, user_id, name, name_hash, color, last_update) VALUES (?,?,?,?,?,?,?)';
const params: SQLiteValue[] = [tagId, bookId, userId, name, nameHash, color, System.timeStampInSeconds()];
const result: RunResult = db.run(query, params);
if (!result || result.changes === 0) {
throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du tag.` : `Error adding tag.`);
}
return tagId;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
} else {
console.error('[SpellTagRepo] An unknown error occurred.');
}
throw new Error(lang === 'fr' ? `Impossible d'ajouter le tag de sort.` : `Unable to add spell tag.`);
}
}
/**
* Updates an existing spell tag.
* @param userId - The unique identifier of the user
* @param tagId - The unique identifier of the tag
* @param name - The encrypted name of the tag
* @param nameHash - The hashed name
* @param color - The optional color hex code
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the update was successful
*/
static updateSpellTag(userId: string, tagId: string, name: string, nameHash: string, color: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE book_spell_tags SET name=?, name_hash=?, color=?, last_update=? WHERE tag_id=? AND user_id=?';
const params: SQLiteValue[] = [name, nameHash, color, System.timeStampInSeconds(), tagId, userId];
const result: RunResult = db.run(query, params);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le tag de sort.` : `Unable to update spell tag.`);
} else {
console.error('[SpellTagRepo] An unknown error occurred.');
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
}
}
}
/**
* Deletes a spell 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 the deletion was successful
*/
static deleteSpellTag(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'DELETE FROM book_spell_tags WHERE tag_id=? AND user_id=?';
const params: SQLiteValue[] = [tagId, userId];
const result: RunResult = db.run(query, params);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de supprimer le tag de sort.` : `Unable to delete spell tag.`);
} else {
console.error('[SpellTagRepo] An unknown error occurred.');
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
}
}
}
/**
* Fetches all spell tags for a book with full table data for sync.
* @param userId - The unique identifier of the user
* @param bookId - The unique identifier of the book
* @param lang - The language for error messages ('fr' or 'en')
* @returns An array of book spell tags table records
*/
static fetchBookSpellTagsTable(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): BookSpellTagsTable[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT tag_id, book_id, user_id, name, name_hash, color, last_update FROM book_spell_tags WHERE user_id=? AND book_id=?';
const params: SQLiteValue[] = [userId, bookId];
return db.all(query, params) as BookSpellTagsTable[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sorts.` : `Unable to retrieve spell tags.`);
} else {
console.error('[SpellTagRepo] An unknown error occurred.');
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
}
}
}
/**
* Fetches a complete spell tag record by its ID.
* @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 The spell tag table record or null
*/
static fetchSpellTagTableById(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): BookSpellTagsTable | null {
try {
const db: Database = System.getDb();
const query: string = 'SELECT tag_id, book_id, user_id, name, name_hash, color, last_update FROM book_spell_tags WHERE user_id=? AND tag_id=?';
const params: SQLiteValue[] = [userId, tagId];
const spellTags: BookSpellTagsTable[] = db.all(query, params) as BookSpellTagsTable[];
return spellTags.length > 0 ? spellTags[0] : null;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer le tag de sort.` : `Unable to retrieve spell tag.`);
} else {
console.error('[SpellTagRepo] An unknown error occurred.');
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
}
}
}
/**
* Fetches all synced spell tags for a user.
* @param userId - The unique identifier of the user
* @param lang - The language for error messages ('fr' or 'en')
* @returns An array of synced spell tag results
*/
static fetchSyncedSpellTags(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSpellTagResult[] {
try {
const db: Database = System.getDb();
const query: string = 'SELECT tag_id, book_id, name, last_update FROM book_spell_tags WHERE user_id=?';
const params: SQLiteValue[] = [userId];
return db.all(query, params) as SyncedSpellTagResult[];
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sorts synchronisés.` : `Unable to retrieve synced spell tags.`);
} else {
console.error('[SpellTagRepo] An unknown error occurred.');
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
}
}
}
/**
* Inserts or updates a spell tag from synchronization data.
* @param tagId - The unique identifier for the tag
* @param bookId - The unique identifier of the book
* @param userId - The unique identifier of the user
* @param name - The encrypted name
* @param nameHash - The hashed name
* @param color - The optional color hex code
* @param lastUpdate - The timestamp of the last update
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the insertion was successful
*/
static insertSyncSpellTag(tagId: string, bookId: string, userId: string, name: string, nameHash: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'INSERT OR REPLACE INTO book_spell_tags (tag_id, book_id, user_id, name, name_hash, color, last_update) VALUES (?,?,?,?,?,?,?)';
const params: SQLiteValue[] = [tagId, bookId, userId, name, nameHash, color, lastUpdate];
const result: RunResult = db.run(query, params);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible d'insérer le tag de sort.` : `Unable to insert spell tag.`);
} else {
console.error('[SpellTagRepo] An unknown error occurred.');
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
}
}
}
/**
* Checks if a spell tag exists.
* @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 the tag exists
*/
static isSpellTagExist(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'SELECT 1 FROM book_spell_tags WHERE tag_id=? AND user_id=?';
const params: SQLiteValue[] = [tagId, userId];
const existenceCheck = db.all(query, params);
return existenceCheck.length > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du tag.` : `Unable to check tag existence.`);
} else {
console.error('[SpellTagRepo] An unknown error occurred.');
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
}
}
}
/**
* Updates a spell tag with timestamp for sync.
* @param userId - The unique identifier of the user
* @param tagId - The unique identifier of the tag
* @param name - The encrypted name
* @param nameHash - The hashed name
* @param color - The optional color hex code
* @param lastUpdate - The timestamp of the last update
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the update was successful
*/
static updateSyncSpellTag(userId: string, tagId: string, name: string, nameHash: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'UPDATE book_spell_tags SET name=?, name_hash=?, color=?, last_update=? WHERE tag_id=? AND user_id=?';
const params: SQLiteValue[] = [name, nameHash, color, lastUpdate, tagId, userId];
const result: RunResult = db.run(query, params);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le tag de sort.` : `Unable to update spell tag.`);
} else {
console.error('[SpellTagRepo] An unknown error occurred.');
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
}
}
}
}

201
electron/ipc/spell.ipc.ts Normal file
View File

@@ -0,0 +1,201 @@
import { ipcMain } from 'electron';
import { createHandler } from '../database/LocalSystem.js';
import Spell from '../database/models/Spell.js';
import type {
SpellProps,
SpellListResponse,
SpellTagProps,
} from '../database/models/Spell.js';
// ==================== INTERFACES ====================
interface SpellPost {
id?: string;
name: string;
description: string;
appearance: string;
tags: string[];
powerLevel?: string | null;
components?: string | null;
limitations?: string | null;
notes?: string | null;
}
interface GetSpellListData {
bookid: string;
}
interface GetSpellTagsData {
bookid: string;
}
interface GetSpellDetailData {
spellid: string;
}
interface CreateSpellData {
bookId: string;
spell: SpellPost;
}
interface UpdateSpellData {
spellId: string;
spell: SpellPost;
}
interface DeleteSpellData {
spellId: string;
}
interface CreateTagData {
bookId: string;
name: string;
color?: string | null;
}
interface UpdateTagData {
tagId: string;
name: string;
color?: string | null;
}
interface DeleteTagData {
tagId: string;
bookId: string;
}
// ==================== SPELL HANDLERS ====================
// GET /spell/list
ipcMain.handle(
'db:spell:list',
createHandler<GetSpellListData, SpellListResponse>(
function (userId: string, data: GetSpellListData, lang: 'fr' | 'en'): SpellListResponse {
return Spell.getSpellList(userId, data.bookid, lang);
},
),
);
// GET /spell/tags
ipcMain.handle(
'db:spell:tags',
createHandler<GetSpellTagsData, SpellTagProps[]>(
function (userId: string, data: GetSpellTagsData, lang: 'fr' | 'en'): SpellTagProps[] {
return Spell.getSpellTags(userId, data.bookid, lang);
},
),
);
// GET /spell/detail
ipcMain.handle(
'db:spell:detail',
createHandler<GetSpellDetailData, SpellProps>(
function (userId: string, data: GetSpellDetailData, lang: 'fr' | 'en'): SpellProps {
return Spell.getSpellDetail(userId, data.spellid, lang);
},
),
);
// POST /spell/add
ipcMain.handle(
'db:spell:create',
createHandler<CreateSpellData, string>(
function (userId: string, data: CreateSpellData, lang: 'fr' | 'en'): string {
const spell: SpellPost = data.spell;
const result: SpellProps = Spell.addSpell(
userId,
data.bookId,
spell.name,
spell.description,
spell.appearance,
spell.tags || [],
spell.powerLevel || null,
spell.components || null,
spell.limitations || null,
spell.notes || null,
spell.id,
lang,
);
return result.id;
},
),
);
// PUT /spell/update
ipcMain.handle(
'db:spell:update',
createHandler<UpdateSpellData, boolean>(
function (userId: string, data: UpdateSpellData, lang: 'fr' | 'en'): boolean {
const spell: SpellPost = data.spell;
return Spell.updateSpell(
userId,
data.spellId,
spell.name,
spell.description,
spell.appearance,
spell.tags || [],
spell.powerLevel || null,
spell.components || null,
spell.limitations || null,
spell.notes || null,
lang,
);
},
),
);
// DELETE /spell/delete
ipcMain.handle(
'db:spell:delete',
createHandler<DeleteSpellData, boolean>(
function (userId: string, data: DeleteSpellData, lang: 'fr' | 'en'): boolean {
return Spell.deleteSpell(userId, data.spellId, lang);
},
),
);
// ==================== SPELL TAG HANDLERS ====================
// POST /spell/tag/add
ipcMain.handle(
'db:spell:tag:create',
createHandler<CreateTagData, string>(
function (userId: string, data: CreateTagData, lang: 'fr' | 'en'): string {
const result: SpellTagProps = Spell.addSpellTag(
userId,
data.bookId,
data.name,
data.color || null,
undefined,
lang,
);
return result.id;
},
),
);
// PUT /spell/tag/update
ipcMain.handle(
'db:spell:tag:update',
createHandler<UpdateTagData, boolean>(
function (userId: string, data: UpdateTagData, lang: 'fr' | 'en'): boolean {
return Spell.updateSpellTag(
userId,
data.tagId,
data.name,
data.color || null,
lang,
);
},
),
);
// DELETE /spell/tag/delete
ipcMain.handle(
'db:spell:tag:delete',
createHandler<DeleteTagData, boolean>(
function (userId: string, data: DeleteTagData, lang: 'fr' | 'en'): boolean {
return Spell.deleteSpellTag(userId, data.tagId, data.bookId, lang);
},
),
);