Add character deletion functionality with confirmation workflow

- Added `handleDeleteCharacter` method to handle character deletion with confirmation prompts.
- Updated `CharacterComponent` and `CharacterDetail` to include delete button and related logic.
- Localized new strings for character deletion (e.g., confirmation prompts, success/error messages).
- Enhanced database repository methods (`deleteCharacter`) to handle character deletion securely.
- Improved synchronization workflows to accommodate character deletion.
This commit is contained in:
natreex
2026-01-22 15:09:04 -05:00
parent 9461eb6120
commit 4e462670a9
16 changed files with 383 additions and 59 deletions

View File

@@ -22,6 +22,9 @@ import {
} from "../repositories/location.repository.js";
import { BookPlotPointsTable } from "../repositories/plotpoint.repository.js";
import { BookWorldElementsTable, BookWorldTable } from "../repositories/world.repository.js";
import { BookSpellsTable } from "../repositories/spell.repo.js";
import { BookSpellTagsTable } from "../repositories/spelltag.repo.js";
import { SyncedSpell, SyncedSpellTag } from "./Spell.js";
import { CompleteChapterContent, SyncedChapter } from "./Chapter.js";
import { SyncedCharacter } from "./Character.js";
import { SyncedLocation } from "./Location.js";
@@ -42,6 +45,7 @@ export interface BookToolsSettings {
characters: boolean;
worlds: boolean;
locations: boolean;
spells: boolean;
}
export interface BookProps {
@@ -79,6 +83,8 @@ export interface CompleteBook {
locationElements: LocationElementTable[];
locationSubElements: LocationSubElementTable[];
bookTools: BookToolsTable[];
spells: BookSpellsTable[];
spellTags: BookSpellTagsTable[];
}
export interface SyncedBook {
@@ -98,6 +104,8 @@ export interface SyncedBook {
guideLine: SyncedGuideLine | null;
aiGuideLine: SyncedAIGuideLine | null;
bookTools: SyncedBookTools | null;
spells: SyncedSpell[];
spellTags: SyncedSpellTag[];
}
export interface BookSyncCompare {
@@ -119,6 +127,8 @@ export interface BookSyncCompare {
guideLine: boolean;
aiGuideLine: boolean;
bookTools: boolean;
spells: string[];
spellTags: string[];
}
export interface CompleteBookData {
@@ -272,7 +282,8 @@ export default class Book {
tools: {
characters: bookTools ? bookTools.characters_enabled === 1 : false,
worlds: bookTools ? bookTools.worlds_enabled === 1 : false,
locations: bookTools ? bookTools.locations_enabled === 1 : false
locations: bookTools ? bookTools.locations_enabled === 1 : false,
spells: bookTools ? bookTools.spells_enabled === 1 : false
}
};
}
@@ -310,8 +321,8 @@ export default class Book {
return BookRepo.deleteBook(userId, bookId, lang);
}
public static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters' | 'worlds' | 'locations', enabled: boolean, lang: 'fr' | 'en' = 'fr'): boolean {
const columnName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' = `${toolName}_enabled` as 'characters_enabled' | 'worlds_enabled' | 'locations_enabled';
public static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters' | 'worlds' | 'locations' | 'spells', enabled: boolean, lang: 'fr' | 'en' = 'fr'): boolean {
const columnName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' | 'spells_enabled' = `${toolName}_enabled` as 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' | 'spells_enabled';
return BookRepo.updateBookToolSetting(userId, bookId, columnName, enabled, System.timeStampInSeconds(), lang);
}

View File

@@ -211,6 +211,17 @@ export default class Character {
return CharacterRepo.deleteAttribute(userId, attributeId, lang);
}
/**
* Deletes a character and all its related data.
* @param userId - The unique identifier of the user
* @param characterId - The unique identifier of the character to delete
* @param lang - The language code for localization (defaults to 'fr')
* @returns True if the deletion was successful
*/
static deleteCharacter(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean {
return CharacterRepo.deleteCharacter(userId, characterId, lang);
}
/**
* Retrieves all attributes for a specific character, grouped by type.
* Decrypts attribute data using the user's encryption key.

View File

@@ -28,6 +28,8 @@ import GuidelineRepo, {
BookGuideLineTable
} from "../repositories/guideline.repository.js";
import IssueRepository, {BookIssuesTable} from "../repositories/issue.repository.js";
import SpellRepo, {BookSpellsTable} from "../repositories/spell.repo.js";
import SpellTagRepo, {BookSpellTagsTable} from "../repositories/spelltag.repo.js";
export default class Download {
/**
@@ -198,8 +200,54 @@ export default class Download {
});
if (!issuesInserted) return false;
return data.bookTools.every((bookTool: BookToolsTable): boolean => {
return BookRepo.insertSyncBookTools(bookTool.book_id, userId, bookTool.characters_enabled, bookTool.worlds_enabled, bookTool.locations_enabled, bookTool.last_update, lang);
const bookToolsInserted: boolean = data.bookTools.every((bookTool: BookToolsTable): boolean => {
return BookRepo.insertSyncBookTools(bookTool.book_id, userId, bookTool.characters_enabled, bookTool.worlds_enabled, bookTool.locations_enabled, bookTool.spells_enabled, bookTool.last_update, lang);
});
if (!bookToolsInserted) return false;
const spellTagsInserted: boolean = data.spellTags.every((spellTag: BookSpellTagsTable): boolean => {
const encryptedTagName: string = System.encryptDataWithUserKey(spellTag.name, userEncryptionKey);
return SpellTagRepo.insertSyncSpellTag(
spellTag.tag_id,
spellTag.book_id,
userId,
encryptedTagName,
spellTag.name_hash,
spellTag.color,
spellTag.last_update,
lang
);
});
if (!spellTagsInserted) return false;
const spellsInserted: boolean = data.spells.every((spell: BookSpellsTable): boolean => {
const encryptedName: string = System.encryptDataWithUserKey(spell.name, userEncryptionKey);
const encryptedDescription: string = System.encryptDataWithUserKey(spell.description, userEncryptionKey);
const encryptedAppearance: string = System.encryptDataWithUserKey(spell.appearance, userEncryptionKey);
const encryptedTags: string = System.encryptDataWithUserKey(spell.tags, userEncryptionKey);
const encryptedPowerLevel: string | null = spell.power_level ? System.encryptDataWithUserKey(spell.power_level, userEncryptionKey) : null;
const encryptedComponents: string | null = spell.components ? System.encryptDataWithUserKey(spell.components, userEncryptionKey) : null;
const encryptedLimitations: string | null = spell.limitations ? System.encryptDataWithUserKey(spell.limitations, userEncryptionKey) : null;
const encryptedNotes: string | null = spell.notes ? System.encryptDataWithUserKey(spell.notes, userEncryptionKey) : null;
return SpellRepo.insertSyncSpell(
spell.spell_id,
spell.book_id,
userId,
encryptedName,
spell.name_hash,
encryptedDescription,
encryptedAppearance,
encryptedTags,
encryptedPowerLevel,
encryptedComponents,
encryptedLimitations,
encryptedNotes,
spell.last_update,
lang
);
});
if (!spellsInserted) return false;
return true;
}
}

View File

@@ -1,6 +1,9 @@
import { getUserEncryptionKey } from "../keyManager.js";
import System from "../System.js";
import { BookSyncCompare, CompleteBook, SyncedBook, SyncedBookTools } from "./Book.js";
import { SyncedSpell, SyncedSpellTag } from "./Spell.js";
import SpellRepo, { BookSpellsTable, SyncedSpellResult } from "../repositories/spell.repo.js";
import SpellTagRepo, { BookSpellTagsTable, SyncedSpellTagResult } from "../repositories/spelltag.repo.js";
import BookRepo, { EritBooksTable, SyncedBookResult, BookToolsTable, SyncedBookToolsResult } from "../repositories/book.repository.js";
import ChapterRepo, {
BookChapterInfosTable,
@@ -87,6 +90,8 @@ export default class Sync {
const decryptedGuideLines: BookGuideLineTable[] = [];
const decryptedAIGuideLines: BookAIGuideLineTable[] = [];
const decryptedIssues: BookIssuesTable[] = [];
const decryptedSpells: BookSpellsTable[] = [];
const decryptedSpellTags: BookSpellTagsTable[] = [];
const actSummaryIds: string[] = syncCompareData.actSummaries;
const chapterIds: string[] = syncCompareData.chapters;
@@ -102,6 +107,8 @@ export default class Sync {
const worldIds: string[] = syncCompareData.worlds;
const worldElementIds: string[] = syncCompareData.worldElements;
const issueIds: string[] = syncCompareData.issues;
const spellIds: string[] = syncCompareData.spells;
const spellTagIds: string[] = syncCompareData.spellTags;
if (actSummaryIds && actSummaryIds.length > 0) {
for (const actSummaryId of actSummaryIds) {
@@ -338,6 +345,37 @@ export default class Sync {
}
}
if (spellTagIds && spellTagIds.length > 0) {
for (const spellTagId of spellTagIds) {
const spellTagRecord: BookSpellTagsTable | null = SpellTagRepo.fetchSpellTagTableById(userId, spellTagId, lang);
if (spellTagRecord) {
decryptedSpellTags.push({
...spellTagRecord,
name: System.decryptDataWithUserKey(spellTagRecord.name, userEncryptionKey)
});
}
}
}
if (spellIds && spellIds.length > 0) {
for (const spellId of spellIds) {
const spellRecord: BookSpellsTable | null = SpellRepo.fetchSpellTableById(userId, spellId, lang);
if (spellRecord) {
decryptedSpells.push({
...spellRecord,
name: System.decryptDataWithUserKey(spellRecord.name, userEncryptionKey),
description: System.decryptDataWithUserKey(spellRecord.description, userEncryptionKey),
appearance: System.decryptDataWithUserKey(spellRecord.appearance, userEncryptionKey),
tags: System.decryptDataWithUserKey(spellRecord.tags, userEncryptionKey),
power_level: spellRecord.power_level ? System.decryptDataWithUserKey(spellRecord.power_level, userEncryptionKey) : null,
components: spellRecord.components ? System.decryptDataWithUserKey(spellRecord.components, userEncryptionKey) : null,
limitations: spellRecord.limitations ? System.decryptDataWithUserKey(spellRecord.limitations, userEncryptionKey) : null,
notes: spellRecord.notes ? System.decryptDataWithUserKey(spellRecord.notes, userEncryptionKey) : null
});
}
}
}
const bookResults: EritBooksTable[] = await BookRepo.fetchCompleteBookById(syncCompareData.id, lang);
if (bookResults.length > 0) {
const bookRecord: EritBooksTable = bookResults[0];
@@ -371,7 +409,9 @@ export default class Sync {
guideLine: decryptedGuideLines,
aiGuideLine: decryptedAIGuideLines,
issues: decryptedIssues,
bookTools: bookTools
bookTools: bookTools,
spells: decryptedSpells,
spellTags: decryptedSpellTags
};
}
@@ -730,13 +770,56 @@ export default class Sync {
if (completeBook.bookTools && completeBook.bookTools.length > 0) {
for (const serverBookTool of completeBook.bookTools) {
const success: boolean = BookRepo.insertSyncBookTools(serverBookTool.book_id, userId, serverBookTool.characters_enabled, serverBookTool.worlds_enabled, serverBookTool.locations_enabled, serverBookTool.last_update, lang);
const success: boolean = BookRepo.insertSyncBookTools(bookId, userId, serverBookTool.characters_enabled, serverBookTool.worlds_enabled, serverBookTool.locations_enabled, serverBookTool.spells_enabled, serverBookTool.last_update, lang);
if (!success) {
return false;
}
}
}
if (completeBook.spellTags && completeBook.spellTags.length > 0) {
for (const serverSpellTag of completeBook.spellTags) {
const spellTagExists: boolean = SpellTagRepo.isSpellTagExist(userId, serverSpellTag.tag_id, lang);
const encryptedName: string = System.encryptDataWithUserKey(serverSpellTag.name, userEncryptionKey);
if (spellTagExists) {
const updateSuccessful: boolean = SpellTagRepo.updateSyncSpellTag(userId, serverSpellTag.tag_id, encryptedName, serverSpellTag.name_hash, serverSpellTag.color, serverSpellTag.last_update, lang);
if (!updateSuccessful) {
return false;
}
} else {
const insertSuccessful: boolean = SpellTagRepo.insertSyncSpellTag(serverSpellTag.tag_id, serverSpellTag.book_id, userId, encryptedName, serverSpellTag.name_hash, serverSpellTag.color, serverSpellTag.last_update, lang);
if (!insertSuccessful) {
return false;
}
}
}
}
if (completeBook.spells && completeBook.spells.length > 0) {
for (const serverSpell of completeBook.spells) {
const spellExists: boolean = SpellRepo.isSpellExist(userId, serverSpell.spell_id, lang);
const encryptedName: string = System.encryptDataWithUserKey(serverSpell.name, userEncryptionKey);
const encryptedDescription: string = System.encryptDataWithUserKey(serverSpell.description, userEncryptionKey);
const encryptedAppearance: string = System.encryptDataWithUserKey(serverSpell.appearance, userEncryptionKey);
const encryptedTags: string = System.encryptDataWithUserKey(serverSpell.tags, userEncryptionKey);
const encryptedPowerLevel: string | null = serverSpell.power_level ? System.encryptDataWithUserKey(serverSpell.power_level, userEncryptionKey) : null;
const encryptedComponents: string | null = serverSpell.components ? System.encryptDataWithUserKey(serverSpell.components, userEncryptionKey) : null;
const encryptedLimitations: string | null = serverSpell.limitations ? System.encryptDataWithUserKey(serverSpell.limitations, userEncryptionKey) : null;
const encryptedNotes: string | null = serverSpell.notes ? System.encryptDataWithUserKey(serverSpell.notes, userEncryptionKey) : null;
if (spellExists) {
const updateSuccessful: boolean = SpellRepo.updateSyncSpell(userId, serverSpell.spell_id, encryptedName, serverSpell.name_hash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, serverSpell.last_update, lang);
if (!updateSuccessful) {
return false;
}
} else {
const insertSuccessful: boolean = SpellRepo.insertSyncSpell(serverSpell.spell_id, serverSpell.book_id, userId, encryptedName, serverSpell.name_hash, encryptedDescription, encryptedAppearance, encryptedTags, encryptedPowerLevel, encryptedComponents, encryptedLimitations, encryptedNotes, serverSpell.last_update, lang);
if (!insertSuccessful) {
return false;
}
}
}
}
return true;
}
@@ -767,7 +850,9 @@ export default class Sync {
allIssues,
allActSummaries,
allGuidelines,
allAIGuidelines
allAIGuidelines,
allSpells,
allSpellTags
]: [
SyncedBookResult[],
SyncedChapterResult[],
@@ -785,7 +870,9 @@ export default class Sync {
SyncedIssueResult[],
SyncedActSummaryResult[],
SyncedGuideLineResult[],
SyncedAIGuideLineResult[]
SyncedAIGuideLineResult[],
SyncedSpellResult[],
SyncedSpellTagResult[]
] = await Promise.all([
BookRepo.fetchSyncedBooks(userId, lang),
ChapterRepo.fetchSyncedChapters(userId, lang),
@@ -803,7 +890,9 @@ export default class Sync {
IssueRepository.fetchSyncedIssues(userId, lang),
ActRepository.fetchSyncedActSummaries(userId, lang),
GuidelineRepo.fetchSyncedGuideLine(userId, lang),
GuidelineRepo.fetchSyncedAIGuideLine(userId, lang)
GuidelineRepo.fetchSyncedAIGuideLine(userId, lang),
SpellRepo.fetchSyncedSpells(userId, lang),
SpellTagRepo.fetchSyncedSpellTags(userId, lang)
]);
return allBooks.map((bookRecord: SyncedBookResult): SyncedBook => {
@@ -958,6 +1047,22 @@ export default class Sync {
lastUpdate: bookToolsQuery.last_update
} : null;
const bookSpells: SyncedSpell[] = allSpells
.filter((spellRecord: SyncedSpellResult): boolean => spellRecord.book_id === currentBookId)
.map((spellRecord: SyncedSpellResult): SyncedSpell => ({
id: spellRecord.spell_id,
name: System.decryptDataWithUserKey(spellRecord.name, userEncryptionKey),
lastUpdate: spellRecord.last_update
}));
const bookSpellTags: SyncedSpellTag[] = allSpellTags
.filter((spellTagRecord: SyncedSpellTagResult): boolean => spellTagRecord.book_id === currentBookId)
.map((spellTagRecord: SyncedSpellTagResult): SyncedSpellTag => ({
id: spellTagRecord.tag_id,
name: System.decryptDataWithUserKey(spellTagRecord.name, userEncryptionKey),
lastUpdate: spellTagRecord.last_update
}));
return {
id: currentBookId,
type: bookRecord.type,
@@ -974,7 +1079,9 @@ export default class Sync {
actSummaries: bookActSummaries,
guideLine: bookGuideLine,
aiGuideLine: bookAIGuideLine,
bookTools: bookTools
bookTools: bookTools,
spells: bookSpells,
spellTags: bookSpellTags
};
});
}

View File

@@ -25,6 +25,8 @@ import WorldRepository, {
BookWorldTable
} from "../repositories/world.repository.js";
import ChapterContentRepository, { BookChapterContentTable } from "../repositories/chaptercontent.repository.js";
import SpellRepo, { BookSpellsTable } from "../repositories/spell.repo.js";
import SpellTagRepo, { BookSpellTagsTable } from "../repositories/spelltag.repo.js";
export default class Upload {
/**
@@ -52,7 +54,9 @@ export default class Upload {
encryptedLocations,
encryptedPlotPoints,
encryptedWorlds,
bookToolsData
bookToolsData,
encryptedSpells,
encryptedSpellTags
]: [
EritBooksTable[],
BookActSummariesTable[],
@@ -65,7 +69,9 @@ export default class Upload {
BookLocationTable[],
BookPlotPointsTable[],
BookWorldTable[],
BookToolsTable | null
BookToolsTable | null,
BookSpellsTable[],
BookSpellTagsTable[]
] = await Promise.all([
BookRepo.fetchEritBooksTable(userId, bookId, lang),
ActRepository.fetchBookActSummaries(userId, bookId, lang),
@@ -78,7 +84,9 @@ export default class Upload {
LocationRepo.fetchBookLocations(userId, bookId, lang),
PlotPointRepository.fetchBookPlotPoints(userId, bookId, lang),
WorldRepository.fetchBookWorlds(userId, bookId, lang),
BookRepo.fetchBookTools(userId, bookId, lang)
BookRepo.fetchBookTools(userId, bookId, lang),
SpellRepo.fetchBookSpellsTable(userId, bookId, lang),
SpellTagRepo.fetchBookSpellTagsTable(userId, bookId, lang)
]);
const [
@@ -239,6 +247,23 @@ export default class Upload {
const bookTools: BookToolsTable[] = bookToolsData ? [bookToolsData] : [];
const spells: BookSpellsTable[] = encryptedSpells.map((spell: BookSpellsTable): BookSpellsTable => ({
...spell,
name: System.decryptDataWithUserKey(spell.name, userEncryptionKey),
description: System.decryptDataWithUserKey(spell.description, userEncryptionKey),
appearance: System.decryptDataWithUserKey(spell.appearance, userEncryptionKey),
tags: System.decryptDataWithUserKey(spell.tags, userEncryptionKey),
power_level: spell.power_level ? System.decryptDataWithUserKey(spell.power_level, userEncryptionKey) : null,
components: spell.components ? System.decryptDataWithUserKey(spell.components, userEncryptionKey) : null,
limitations: spell.limitations ? System.decryptDataWithUserKey(spell.limitations, userEncryptionKey) : null,
notes: spell.notes ? System.decryptDataWithUserKey(spell.notes, userEncryptionKey) : null
}));
const spellTags: BookSpellTagsTable[] = encryptedSpellTags.map((spellTag: BookSpellTagsTable): BookSpellTagsTable => ({
...spellTag,
name: System.decryptDataWithUserKey(spellTag.name, userEncryptionKey)
}));
return {
eritBooks,
actSummaries,
@@ -257,7 +282,9 @@ export default class Upload {
worldElements,
locationElements,
locationSubElements,
bookTools
bookTools,
spells,
spellTags
};
}
}

View File

@@ -52,6 +52,7 @@ export interface BookToolsTable extends Record<string, SQLiteValue> {
characters_enabled: number;
worlds_enabled: number;
locations_enabled: number;
spells_enabled: number;
last_update: number;
}
@@ -379,7 +380,7 @@ export default class BookRepo {
static fetchBookTools(userId: string, bookId: string, lang: 'fr' | 'en'): BookToolsTable | null {
try {
const db: Database = System.getDb();
const query: string = 'SELECT book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, last_update FROM book_tools WHERE user_id=? AND book_id=?';
const query: string = 'SELECT book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, spells_enabled, last_update FROM book_tools WHERE user_id=? AND book_id=?';
const params: SQLiteValue[] = [userId, bookId];
const result = db.get(query, params) as BookToolsTable | undefined;
return result ?? null;
@@ -392,7 +393,7 @@ export default class BookRepo {
}
}
static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled', enabled: boolean, lastUpdate: number, lang: 'fr' | 'en'): boolean {
static updateBookToolSetting(userId: string, bookId: string, toolName: 'characters_enabled' | 'worlds_enabled' | 'locations_enabled' | 'spells_enabled', enabled: boolean, lastUpdate: number, lang: 'fr' | 'en'): boolean {
const enabledValue: number = enabled ? 1 : 0;
try {
const db: Database = System.getDb();
@@ -404,8 +405,9 @@ export default class BookRepo {
const charactersValue: number = toolName === 'characters_enabled' ? enabledValue : 0;
const worldsValue: number = toolName === 'worlds_enabled' ? enabledValue : 0;
const locationsValue: number = toolName === 'locations_enabled' ? enabledValue : 0;
const insertQuery: string = 'INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, last_update) VALUES (?, ?, ?, ?, ?, ?)';
const insertResult: RunResult = db.run(insertQuery, [bookId, userId, charactersValue, worldsValue, locationsValue, lastUpdate]);
const spellsValue: number = toolName === 'spells_enabled' ? enabledValue : 0;
const insertQuery: string = 'INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, spells_enabled, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)';
const insertResult: RunResult = db.run(insertQuery, [bookId, userId, charactersValue, worldsValue, locationsValue, spellsValue, lastUpdate]);
return insertResult.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
@@ -420,11 +422,11 @@ export default class BookRepo {
* Upserts book tools settings during sync.
* Inserts if not exists, updates if exists.
*/
static insertSyncBookTools(bookId: string, userId: string, charactersEnabled: number, worldsEnabled: number, locationsEnabled: number, lastUpdate: number, lang: 'fr' | 'en'): boolean {
static insertSyncBookTools(bookId: string, userId: string, charactersEnabled: number, worldsEnabled: number, locationsEnabled: number, spellsEnabled: number, lastUpdate: number, lang: 'fr' | 'en'): boolean {
try {
const db: Database = System.getDb();
const query: string = 'INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, last_update) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (book_id, user_id) DO UPDATE SET characters_enabled = excluded.characters_enabled, worlds_enabled = excluded.worlds_enabled, locations_enabled = excluded.locations_enabled, last_update = excluded.last_update';
const params: SQLiteValue[] = [bookId, userId, charactersEnabled, worldsEnabled, locationsEnabled, lastUpdate];
const query: string = 'INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, spells_enabled, last_update) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT (book_id, user_id) DO UPDATE SET characters_enabled = excluded.characters_enabled, worlds_enabled = excluded.worlds_enabled, locations_enabled = excluded.locations_enabled, spells_enabled = excluded.spells_enabled, last_update = excluded.last_update';
const params: SQLiteValue[] = [bookId, userId, charactersEnabled, worldsEnabled, locationsEnabled, spellsEnabled, lastUpdate];
db.run(query, params);
return true;
} catch (error: unknown) {

View File

@@ -198,6 +198,32 @@ export default class CharacterRepo {
}
}
/**
* Deletes a character and all its related data (attributes) from the database.
* @param userId - The unique identifier of the user
* @param characterId - The unique identifier of the character to delete
* @param lang - The language for error messages ('fr' or 'en')
* @returns True if the deletion was successful, false otherwise
*/
static deleteCharacter(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean {
try {
const db: Database = System.getDb();
const deleteAttributesQuery: string = 'DELETE FROM `book_characters_attributes` WHERE `character_id`=? AND `user_id`=?';
db.run(deleteAttributesQuery, [characterId, userId]);
const deleteCharacterQuery: string = 'DELETE FROM `book_characters` WHERE `character_id`=? AND `user_id`=?';
const result: RunResult = db.run(deleteCharacterQuery, [characterId, userId]);
return result.changes > 0;
} catch (error: unknown) {
if (error instanceof Error) {
console.error(`DB Error: ${error.message}`);
throw new Error(lang === 'fr' ? `Impossible de supprimer le personnage.` : `Unable to delete character.`);
} else {
console.error("An unknown error occurred.");
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred.");
}
}
}
/**
* Deletes a character attribute from the database.
* @param userId - The unique identifier of the user

View File

@@ -1,4 +1,5 @@
import sqlite3 from 'node-sqlite3-wasm';
import { app } from 'electron';
type Database = sqlite3.Database;
@@ -18,11 +19,9 @@ const schemaVersion = 1;
* DEV ONLY - S'exécute à chaque refresh, pas besoin de version
* Mets ta query, test, efface après
*/
const devQueries: string[] = [
const devQueries: string[] = [];
];
const isDev = process.env.NODE_ENV === 'development';
const isDev:boolean = !app.isPackaged;
function columnExists(db: Database, table: string, column: string): boolean {
const result = db.all(`PRAGMA table_info(${table})`) as { name: string }[];

View File

@@ -74,4 +74,15 @@ ipcMain.handle('db:character:update', createHandler<UpdateCharacterData, boolean
return Character.updateCharacter(userId, data.character, lang);
}
)
);
// DELETE /character/delete - Delete character
interface DeleteCharacterData {
characterId: string;
}
ipcMain.handle('db:character:delete', createHandler<DeleteCharacterData, boolean>(
function(userId: string, data: DeleteCharacterData, lang: 'fr' | 'en'): boolean {
return Character.deleteCharacter(userId, data.characterId, lang);
}
)
);

View File

@@ -15,6 +15,7 @@ import './ipc/chapter.ipc.js';
import './ipc/character.ipc.js';
import './ipc/location.ipc.js';
import './ipc/offline.ipc.js';
import './ipc/spell.ipc.js';
// Fix pour __dirname en ES modules
const __filename = fileURLToPath(import.meta.url);