From 687c1d582c0ce04381fa854c231d3c34edee1c34 Mon Sep 17 00:00:00 2001 From: natreex Date: Sun, 5 Apr 2026 19:18:42 -0400 Subject: [PATCH] Remove Act, AutoUpdater, and Book IPC modules alongside associated database logic. --- app/login/login/LoginForm.tsx | 2 +- app/login/login/page.tsx | 2 +- app/login/reset-password/page.tsx | 6 +- components/ShortStoryGenerator.tsx | 1 + .../book/settings/BasicInformationSetting.tsx | 29 +- components/book/settings/ExportSetting.tsx | 1 + components/book/settings/story/Act.tsx | 4 +- components/editor/DraftCompanion.tsx | 1 + components/editor/UserEditorSetting.tsx | 2 +- components/ghostwriter/GhostWriter.tsx | 3 +- components/layout/ScribeFooterBar.tsx | 2 +- components/leftbar/ScribeChapterComponent.tsx | 2 +- components/quillsense/modes/InspireMe.tsx | 2 +- electron/autoUpdater.ts | 75 - electron/database/LocalSystem.ts | 105 - electron/database/System.ts | 71 - electron/database/database.service.ts | 93 - electron/database/encryption.ts | 99 - electron/database/keyManager.ts | 52 - electron/database/models/Act.ts | 183 - electron/database/models/Book.ts | 730 --- electron/database/models/Chapter.ts | 664 --- electron/database/models/Character.ts | 515 -- electron/database/models/Content.ts | 193 - electron/database/models/Cover.ts | 62 - electron/database/models/Download.ts | 266 - electron/database/models/EpubStyle.ts | 23 - electron/database/models/Export.ts | 211 - electron/database/models/GuideLine.ts | 218 - electron/database/models/Incident.ts | 112 - electron/database/models/Issue.ts | 107 - electron/database/models/Location.ts | 383 -- electron/database/models/Model.ts | 278 -- electron/database/models/PlotPoint.ts | 113 - electron/database/models/RemovedItem.ts | 41 - electron/database/models/Series.ts | 259 - electron/database/models/SeriesCharacter.ts | 287 -- electron/database/models/SeriesLocation.ts | 170 - electron/database/models/SeriesSpell.ts | 222 - electron/database/models/SeriesSync.ts | 1023 ---- electron/database/models/SeriesWorld.ts | 196 - electron/database/models/Spell.ts | 377 -- electron/database/models/Sync.ts | 1277 ----- electron/database/models/Upload.ts | 301 -- electron/database/models/User.ts | 302 -- electron/database/models/World.ts | 287 -- .../database/repositories/act.repository.ts | 240 - .../database/repositories/book.repository.ts | 495 -- .../repositories/chapter.repository.ts | 755 --- .../repositories/chaptercontent.repository.ts | 371 -- .../repositories/character.repository.ts | 693 --- .../repositories/guideline.repository.ts | 567 --- .../repositories/incident.repository.ts | 276 -- .../database/repositories/issue.repository.ts | 277 -- .../repositories/location.repository.ts | 900 ---- .../repositories/plotpoint.repository.ts | 288 -- .../repositories/removed-items.repository.ts | 158 - .../repositories/series-character.repo.ts | 541 --- .../repositories/series-location.repo.ts | 813 ---- .../repositories/series-spell.repo.ts | 647 --- .../database/repositories/series-sync.repo.ts | 258 - .../repositories/series-world.repo.ts | 555 --- electron/database/repositories/series.repo.ts | 553 --- electron/database/repositories/spell.repo.ts | 378 -- .../database/repositories/spelltag.repo.ts | 293 -- .../database/repositories/user.repository.ts | 235 - .../database/repositories/world.repository.ts | 583 --- electron/database/schema.ts | 1320 ----- electron/ipc/book.ipc.ts | 501 -- electron/ipc/chapter.ipc.ts | 169 - electron/ipc/character.ipc.ts | 92 - electron/ipc/location.ipc.ts | 127 - electron/ipc/offline.ipc.ts | 155 - electron/ipc/series-character.ipc.ts | 85 - electron/ipc/series-location.ipc.ts | 88 - electron/ipc/series-spell.ipc.ts | 113 - electron/ipc/series-sync.ipc.ts | 54 - electron/ipc/series-world.ipc.ts | 77 - electron/ipc/series.ipc.ts | 119 - electron/ipc/spell.ipc.ts | 207 - electron/ipc/tombstone.ipc.ts | 122 - electron/ipc/user.ipc.ts | 26 - electron/main.ts | 826 ---- electron/preload.ts | 52 - electron/storage/SecureStorage.ts | 229 - index.html | 13 + lib/api/client.ts | 197 +- lib/configs.ts | 2 +- lib/crashReporter.ts | 7 +- lib/locales/en.json | 6 - lib/locales/fr.json | 6 - next.config.ts | 14 - package-lock.json | 4267 +---------------- package.json | 76 +- src-tauri/src/crypto/key_manager.rs | 517 +- src-tauri/src/lib.rs | 1 + tsconfig.electron.json | 30 - tsconfig.preload.json | 20 - vite.config.ts | 23 + 99 files changed, 500 insertions(+), 28269 deletions(-) delete mode 100644 electron/autoUpdater.ts delete mode 100644 electron/database/LocalSystem.ts delete mode 100644 electron/database/System.ts delete mode 100644 electron/database/database.service.ts delete mode 100644 electron/database/encryption.ts delete mode 100644 electron/database/keyManager.ts delete mode 100644 electron/database/models/Act.ts delete mode 100644 electron/database/models/Book.ts delete mode 100644 electron/database/models/Chapter.ts delete mode 100644 electron/database/models/Character.ts delete mode 100755 electron/database/models/Content.ts delete mode 100644 electron/database/models/Cover.ts delete mode 100644 electron/database/models/Download.ts delete mode 100755 electron/database/models/EpubStyle.ts delete mode 100644 electron/database/models/Export.ts delete mode 100644 electron/database/models/GuideLine.ts delete mode 100644 electron/database/models/Incident.ts delete mode 100644 electron/database/models/Issue.ts delete mode 100644 electron/database/models/Location.ts delete mode 100644 electron/database/models/Model.ts delete mode 100644 electron/database/models/PlotPoint.ts delete mode 100644 electron/database/models/RemovedItem.ts delete mode 100644 electron/database/models/Series.ts delete mode 100644 electron/database/models/SeriesCharacter.ts delete mode 100644 electron/database/models/SeriesLocation.ts delete mode 100644 electron/database/models/SeriesSpell.ts delete mode 100644 electron/database/models/SeriesSync.ts delete mode 100644 electron/database/models/SeriesWorld.ts delete mode 100644 electron/database/models/Spell.ts delete mode 100644 electron/database/models/Sync.ts delete mode 100644 electron/database/models/Upload.ts delete mode 100644 electron/database/models/User.ts delete mode 100644 electron/database/models/World.ts delete mode 100644 electron/database/repositories/act.repository.ts delete mode 100644 electron/database/repositories/book.repository.ts delete mode 100644 electron/database/repositories/chapter.repository.ts delete mode 100644 electron/database/repositories/chaptercontent.repository.ts delete mode 100644 electron/database/repositories/character.repository.ts delete mode 100644 electron/database/repositories/guideline.repository.ts delete mode 100644 electron/database/repositories/incident.repository.ts delete mode 100644 electron/database/repositories/issue.repository.ts delete mode 100644 electron/database/repositories/location.repository.ts delete mode 100644 electron/database/repositories/plotpoint.repository.ts delete mode 100644 electron/database/repositories/removed-items.repository.ts delete mode 100644 electron/database/repositories/series-character.repo.ts delete mode 100644 electron/database/repositories/series-location.repo.ts delete mode 100644 electron/database/repositories/series-spell.repo.ts delete mode 100644 electron/database/repositories/series-sync.repo.ts delete mode 100644 electron/database/repositories/series-world.repo.ts delete mode 100644 electron/database/repositories/series.repo.ts delete mode 100644 electron/database/repositories/spell.repo.ts delete mode 100644 electron/database/repositories/spelltag.repo.ts delete mode 100644 electron/database/repositories/user.repository.ts delete mode 100644 electron/database/repositories/world.repository.ts delete mode 100644 electron/database/schema.ts delete mode 100644 electron/ipc/book.ipc.ts delete mode 100644 electron/ipc/chapter.ipc.ts delete mode 100644 electron/ipc/character.ipc.ts delete mode 100644 electron/ipc/location.ipc.ts delete mode 100644 electron/ipc/offline.ipc.ts delete mode 100644 electron/ipc/series-character.ipc.ts delete mode 100644 electron/ipc/series-location.ipc.ts delete mode 100644 electron/ipc/series-spell.ipc.ts delete mode 100644 electron/ipc/series-sync.ipc.ts delete mode 100644 electron/ipc/series-world.ipc.ts delete mode 100644 electron/ipc/series.ipc.ts delete mode 100644 electron/ipc/spell.ipc.ts delete mode 100644 electron/ipc/tombstone.ipc.ts delete mode 100644 electron/ipc/user.ipc.ts delete mode 100644 electron/main.ts delete mode 100644 electron/preload.ts delete mode 100644 electron/storage/SecureStorage.ts create mode 100644 index.html delete mode 100644 next.config.ts delete mode 100644 tsconfig.electron.json delete mode 100644 tsconfig.preload.json create mode 100644 vite.config.ts diff --git a/app/login/login/LoginForm.tsx b/app/login/login/LoginForm.tsx index 233e07e..6dc614c 100755 --- a/app/login/login/LoginForm.tsx +++ b/app/login/login/LoginForm.tsx @@ -58,7 +58,7 @@ export default function LoginForm() { await tauri.loginSuccess(); } catch (e: unknown) { if (e instanceof Error) { - errorMessage(t('loginForm.error.server')); + errorMessage(e.message); } else { errorMessage(t('loginForm.error.unknown')); } diff --git a/app/login/login/page.tsx b/app/login/login/page.tsx index 2b49041..cf3a45a 100755 --- a/app/login/login/page.tsx +++ b/app/login/login/page.tsx @@ -99,7 +99,7 @@ export default function LoginPage() { await tauri.loginSuccess(); } catch (e: unknown) { if (e instanceof Error) { - errorMessage(t('loginForm.error.server')); + errorMessage(e.message); } else { errorMessage(t('loginForm.error.unknown')); } diff --git a/app/login/reset-password/page.tsx b/app/login/reset-password/page.tsx index e45a642..0f30e2e 100755 --- a/app/login/reset-password/page.tsx +++ b/app/login/reset-password/page.tsx @@ -40,7 +40,7 @@ export default function ForgetPasswordPage() { } } catch (e: unknown) { if (e instanceof Error) { - errorMessage(t('resetPassword.error.emailServer')); + errorMessage(e.message); } else { errorMessage(t('resetPassword.error.emailUnknown')); } @@ -60,7 +60,7 @@ export default function ForgetPasswordPage() { } } catch (e: unknown) { if (e instanceof Error) { - errorMessage(t('resetPassword.error.codeServer')); + errorMessage(e.message); } else { errorMessage(t('resetPassword.error.codeUnknown')); } @@ -80,7 +80,7 @@ export default function ForgetPasswordPage() { } } catch (e: unknown) { if (e instanceof Error) { - errorMessage(t('resetPassword.error.passwordServer')); + errorMessage(e.message); } else { errorMessage(t('resetPassword.error.passwordUnknown')); } diff --git a/components/ShortStoryGenerator.tsx b/components/ShortStoryGenerator.tsx index 00f1e4a..b147366 100644 --- a/components/ShortStoryGenerator.tsx +++ b/components/ShortStoryGenerator.tsx @@ -1,3 +1,4 @@ +import {fetch} from "@tauri-apps/plugin-http"; import React, {ChangeEvent, useContext, useEffect, useRef, useState} from 'react'; import { BarChart2, diff --git a/components/book/settings/BasicInformationSetting.tsx b/components/book/settings/BasicInformationSetting.tsx index 2234003..7f273ea 100644 --- a/components/book/settings/BasicInformationSetting.tsx +++ b/components/book/settings/BasicInformationSetting.tsx @@ -3,7 +3,7 @@ import {X} from 'lucide-react'; import IconButton from "@/components/ui/IconButton"; import React, {ChangeEvent, forwardRef, useContext, useImperativeHandle, useState} from "react"; import {apiDelete, apiPost} from "@/lib/api/client"; -import axios, {AxiosResponse} from "axios"; +import {fetch} from "@tauri-apps/plugin-http"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {BookContext, BookContextProps} from "@/context/BookContext"; import {SessionContext, SessionContextProps} from "@/context/SessionContext"; @@ -56,22 +56,17 @@ function BasicInformationSetting(_props: object, ref: React.ForwardedRef = await axios({ - method: "POST", - url: configs.apiUrl + `book/cover?bookid=${bookId}`, - headers: { - 'Authorization': `Bearer ${userToken}`, - }, - params: { - lang: lang, - plateforme: 'web', - }, - data: formData, - responseType: 'arraybuffer' - }); - - const contentType: string = query.headers['content-type'] || 'image/jpeg'; - const blob: Blob = new Blob([query.data], {type: contentType}); + const query: Response = await fetch( + configs.apiUrl + `book/cover?bookid=${bookId}&lang=${lang}&plateforme=desktop`, + { + method: "POST", + headers: {'Authorization': `Bearer ${userToken}`}, + body: formData, + } + ); + + const contentType: string = query.headers.get('content-type') || 'image/jpeg'; + const blob: Blob = new Blob([await query.arrayBuffer()], {type: contentType}); const reader: FileReader = new FileReader(); reader.onloadend = function (): void { diff --git a/components/book/settings/ExportSetting.tsx b/components/book/settings/ExportSetting.tsx index 51cbd1d..4c5eb8b 100644 --- a/components/book/settings/ExportSetting.tsx +++ b/components/book/settings/ExportSetting.tsx @@ -1,4 +1,5 @@ 'use client' +import {fetch} from "@tauri-apps/plugin-http"; import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; import {Check} from 'lucide-react'; import Button from '@/components/ui/Button'; diff --git a/components/book/settings/story/Act.tsx b/components/book/settings/story/Act.tsx index fa3cabb..9c82371 100644 --- a/components/book/settings/story/Act.tsx +++ b/components/book/settings/story/Act.tsx @@ -108,7 +108,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { setNewIncidentTitle(''); } catch (e: unknown) { if (e instanceof Error) { - errorMessage(t('errorAddIncident')); + errorMessage(e.message); } else { errorMessage(t('errorUnknownAddIncident')); } @@ -192,7 +192,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) { setSelectedIncidentId(''); } catch (e: unknown) { if (e instanceof Error) { - errorMessage(t('errorAddPlotPoint')); + errorMessage(e.message); } else { errorMessage(t('errorUnknownAddPlotPoint')); } diff --git a/components/editor/DraftCompanion.tsx b/components/editor/DraftCompanion.tsx index da6ea4e..1ef5290 100644 --- a/components/editor/DraftCompanion.tsx +++ b/components/editor/DraftCompanion.tsx @@ -1,3 +1,4 @@ +import {fetch} from "@tauri-apps/plugin-http"; import React, {ChangeEvent, useContext, useEffect, useState} from "react"; import {Editor, EditorContent, useEditor} from "@tiptap/react"; import StarterKit from "@tiptap/starter-kit"; diff --git a/components/editor/UserEditorSetting.tsx b/components/editor/UserEditorSetting.tsx index f1ab01b..07d0a44 100644 --- a/components/editor/UserEditorSetting.tsx +++ b/components/editor/UserEditorSetting.tsx @@ -95,7 +95,7 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi localStorage.setItem('userEditorSettings', JSON.stringify(settings)); } catch (e: unknown) { if (e instanceof Error) { - errorMessage(t('userEditorSettings.saveError')); + errorMessage(e.message); } else { errorMessage(t('userEditorSettings.unknownError')); } diff --git a/components/ghostwriter/GhostWriter.tsx b/components/ghostwriter/GhostWriter.tsx index 66b902f..92908eb 100644 --- a/components/ghostwriter/GhostWriter.tsx +++ b/components/ghostwriter/GhostWriter.tsx @@ -1,3 +1,4 @@ +import {fetch} from "@tauri-apps/plugin-http"; import React, {ChangeEvent, useContext, useState} from 'react'; import {BookOpen, FileInput, Hash, Palette, Wand2} from 'lucide-react'; import {SessionContext, SessionContextProps} from "@/context/SessionContext"; @@ -76,7 +77,7 @@ export default function GhostWriter() { content = editor?.getText(); } catch (e: unknown) { if (e instanceof Error) { - errorMessage(t('ghostWriter.errorRetrieveContent')); + errorMessage(e.message); } else { errorMessage(t('ghostWriter.errorUnknownRetrieveContent')); } diff --git a/components/layout/ScribeFooterBar.tsx b/components/layout/ScribeFooterBar.tsx index f73f289..504fbb6 100644 --- a/components/layout/ScribeFooterBar.tsx +++ b/components/layout/ScribeFooterBar.tsx @@ -46,7 +46,7 @@ export default function ScribeFooterBar() { setParagraphCount(paragraphs); } catch (e: unknown) { if (e instanceof Error) { - errorMessage(t('errors.wordCountError') + ` (${e.message})`); + errorMessage(e.message); } else { errorMessage(t('errors.wordCountError')); } diff --git a/components/leftbar/ScribeChapterComponent.tsx b/components/leftbar/ScribeChapterComponent.tsx index 5f65d92..2609d94 100644 --- a/components/leftbar/ScribeChapterComponent.tsx +++ b/components/leftbar/ScribeChapterComponent.tsx @@ -135,7 +135,7 @@ export default function ScribeChapterComponent() { }); } catch (e: unknown) { if (e instanceof Error) { - errorMessage(t("scribeChapterComponent.errorChapterUpdateFr")); + errorMessage(e.message); } else { errorMessage(t("scribeChapterComponent.errorChapterUpdateEn")); } diff --git a/components/quillsense/modes/InspireMe.tsx b/components/quillsense/modes/InspireMe.tsx index adf8b43..cca8418 100644 --- a/components/quillsense/modes/InspireMe.tsx +++ b/components/quillsense/modes/InspireMe.tsx @@ -56,7 +56,7 @@ export default function InspireMe({hasKey}: { hasKey: boolean }): React.JSX.Elem content = htmlToText(content); } catch (e: unknown) { if (e instanceof Error) { - errorMessage(t('inspireMe.error.contentRetrieval')); + errorMessage(e.message); } else { errorMessage(t('inspireMe.error.contentRetrievalUnknown')); } diff --git a/electron/autoUpdater.ts b/electron/autoUpdater.ts deleted file mode 100644 index c6ee54a..0000000 --- a/electron/autoUpdater.ts +++ /dev/null @@ -1,75 +0,0 @@ -import pkg from 'electron-updater'; -import type { UpdateInfo } from 'electron-updater'; -const { autoUpdater } = pkg; -import { app, BrowserWindow } from 'electron'; - -const updateCheckInterval = 4 * 60 * 60 * 1000; // 4 heures - -let initialized = false; -let currentWindow: BrowserWindow | null = null; - -export function initAutoUpdater(window: BrowserWindow): void { - currentWindow = window; - - if (!app.isPackaged) { - console.log('[AutoUpdater] Skipped in development mode'); - return; - } - - // Si déjà initialisé, juste mettre à jour la fenêtre cible - if (initialized) { - console.log('[AutoUpdater] Window target updated'); - return; - } - - initialized = true; - - // Config: télécharge auto, installe au quit - autoUpdater.autoDownload = true; - autoUpdater.autoInstallOnAppQuit = true; - - autoUpdater.on('checking-for-update', () => { - console.log('[AutoUpdater] Checking for updates...'); - }); - - autoUpdater.on('update-available', (info: UpdateInfo) => { - console.log('[AutoUpdater] Update available:', info.version); - currentWindow?.webContents.send('update:available', info.version); - }); - - autoUpdater.on('update-not-available', () => { - console.log('[AutoUpdater] App is up to date'); - }); - - autoUpdater.on('download-progress', (progress) => { - const percent = Math.round(progress.percent); - console.log(`[AutoUpdater] Downloading: ${percent}%`); - currentWindow?.webContents.send('update:progress', percent); - }); - - autoUpdater.on('update-downloaded', (info: UpdateInfo) => { - console.log('[AutoUpdater] Update ready:', info.version); - currentWindow?.webContents.send('update:ready', info.version); - }); - - autoUpdater.on('error', (error: Error) => { - console.error('[AutoUpdater] Error:', error.message); - }); - - // Check initial - autoUpdater.checkForUpdates().catch((err) => { - console.error('[AutoUpdater] Check failed:', err.message); - }); - - // Re-check périodique - setInterval(() => { - autoUpdater.checkForUpdates().catch((err) => { - console.error('[AutoUpdater] Periodic check failed:', err.message); - }); - }, updateCheckInterval); -} - -// Pour forcer l'installation immédiate (optionnel, appelable depuis le renderer) -export function installUpdateNow(): void { - autoUpdater.quitAndInstall(false, true); -} \ No newline at end of file diff --git a/electron/database/LocalSystem.ts b/electron/database/LocalSystem.ts deleted file mode 100644 index abf79de..0000000 --- a/electron/database/LocalSystem.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { IpcMainInvokeEvent } from 'electron'; -import { getSecureStorage } from '../storage/SecureStorage.js'; - -// ============================================================ -// SESSION MANAGEMENT - Auto-inject userId and lang -// ============================================================ - -/** - * Get userId from secure storage (OS-encrypted) - * Set during login via 'login-success' event - */ -function getUserIdFromSession(): string | null { - const storage = getSecureStorage(); - return storage.get('userId', null); -} - -/** - * Get lang from secure storage - * Set via 'set-lang' handler, defaults to 'fr' - */ -function getLangFromSession(): 'fr' | 'en' { - const storage = getSecureStorage(); - return storage.get<'fr' | 'en'>('userLang', 'fr') as 'fr' | 'en'; -} - -// ============================================================ -// UNIVERSAL HANDLER - Like a Fastify route -// Automatically injects: userId, lang -// Optional body parameter (for GET, POST, PUT, DELETE) -// Generic return type (void, object, etc.) -// ============================================================ - -/** - * Universal IPC handler - works like a Fastify route - * Automatically injects: userId, lang from session - * - * @template TBody - Request body type (use void for no params) - * @template TReturn - Response type (use void for no return) - * - * @example - * // GET with no params - * ipcMain.handle('db:books:getAll', - * createHandler( - * async (userId, body, lang) => { - * return await Book.getBooks(userId, lang); - * } - * ) - * ); - * // Frontend: invoke('db:books:getAll') - * - * @example - * // GET with 1 param - * ipcMain.handle('db:book:get', - * createHandler( - * async (userId, bookId, lang) => { - * return await Book.getBook(bookId, userId, lang); - * } - * ) - * ); - * // Frontend: invoke('db:book:get', bookId) - * - * @example - * // POST with object body - * ipcMain.handle('db:book:create', - * createHandler( - * async (userId, data, lang) => { - * return await Book.addBook(userId, data, lang); - * } - * ) - * ); - * // Frontend: invoke('db:book:create', { title: '...', ... }) - * - * @example - * // DELETE with void return - * ipcMain.handle('db:book:delete', - * createHandler( - * async (userId, bookId, lang) => { - * await Book.deleteBook(bookId, userId, lang); - * } - * ) - * ); - * // Frontend: invoke('db:book:delete', bookId) - */ -export function createHandler( - handler: (userId: string, body: TBody, lang: 'fr' | 'en') => TReturn | Promise -): (event: IpcMainInvokeEvent, body?: TBody) => Promise { - return async function(event: IpcMainInvokeEvent, body?: TBody): Promise { - const userId = getUserIdFromSession(); - const lang = getLangFromSession(); - - if (!userId) { - throw new Error('User not authenticated'); - } - - try { - return await handler(userId, body as TBody, lang); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`[DB] ${error.message}`); - throw error; - } - throw new Error('An unknown error occurred.'); - } - }; -} diff --git a/electron/database/System.ts b/electron/database/System.ts deleted file mode 100644 index 5b689d6..0000000 --- a/electron/database/System.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { getDatabaseService } from './database.service.js'; -import { encryptDataWithUserKey, decryptDataWithUserKey, hashElement } from './encryption.js'; -import type { Database } from 'node-sqlite3-wasm'; -import crypto from 'crypto'; - -export default class System { - public static getDb(): Database { - const db: Database | null = getDatabaseService().getDb(); - if (!db) { - throw new Error('Database not initialized'); - } - return db; - } - - public static encryptDataWithUserKey(data: string, userKey: string): string { - return encryptDataWithUserKey(data, userKey); - } - - public static timeStampInSeconds(): number { - const date:number = new Date().getTime(); - return Math.floor(date / 1000); - } - - public static decryptDataWithUserKey(encryptedData: string, userKey: string): string { - return decryptDataWithUserKey(encryptedData, userKey); - } - - public static createUniqueId(): string { - return crypto.randomUUID(); - } - - static htmlToText(htmlNode: string): string { - let text: string = htmlNode - .replace(/<\/?p[^>]*>/gi, '\n') - .replace(//gi, '\n') - .replace(/<\/?(span|h[1-6])[^>]*>/gi, ''); - text = text - .replace(/'/g, "'") - .replace(/"/g, '"') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/'/g, "'"); - text = text.replace(/\r?\n\s*\n/g, '\n'); - text = text.replace(/[ \t]+/g, ' '); - - return text.trim(); - } - - public static getCurrentDate(): string { - return new Date().toISOString(); - } - - static dateToMySqlDate(isoDateString: string): string { - const dateObject: Date = new Date(isoDateString); - - function padWithZeroes(value: number): string { - return value.toString().padStart(2, '0'); - } - - const year: number = dateObject.getFullYear(); - const month: string = padWithZeroes(dateObject.getMonth() + 1); - const day: string = padWithZeroes(dateObject.getDate()); - - return `${year}-${month}-${day}`; - } - - public static hashElement(element: string): string { - return hashElement(element); - } -} \ No newline at end of file diff --git a/electron/database/database.service.ts b/electron/database/database.service.ts deleted file mode 100644 index fb6aa3d..0000000 --- a/electron/database/database.service.ts +++ /dev/null @@ -1,93 +0,0 @@ -import sqlite3 from 'node-sqlite3-wasm'; -import path from 'path'; -import { app } from 'electron'; -import { initializeSchema, runMigrations } from './schema.js'; - -// Type alias for compatibility -export type Database = sqlite3.Database; - -/** - * DatabaseService - Manages SQLite database connection ONLY - * No business logic, no CRUD operations - * Just connection management and encryption key storage - */ -export class DatabaseService { - private db: Database | null = null; - private userEncryptionKey: string | null = null; - private userId: string | null = null; - - constructor() {} - - /** - * Initialize the database for a specific user - * @param userId - User ID for encryption key - * @param encryptionKey - User's encryption key (generated at first login) - */ - initialize(userId: string, encryptionKey: string): void { - if (this.db) { - this.close(); - } - - const userDataPath:string = app.getPath('userData'); - const dbPath:string = path.join(userDataPath, `eritors-local.db`); - - this.db = new sqlite3.Database(dbPath); - this.userEncryptionKey = encryptionKey; - this.userId = userId; - - initializeSchema(this.db); - - runMigrations(this.db); - } - - /** - * Close the database connection - */ - close(): void { - if (this.db) { - this.db.close(); - this.db = null; - this.userEncryptionKey = null; - this.userId = null; - } - } - - /** - * Check if database is initialized - */ - isInitialized(): boolean { - return this.db !== null && this.userEncryptionKey !== null; - } - - /** - * Get database connection - * Use this in repositories and model classes - */ - getDb(): Database | null { - return this.db; - } - - /** - * Get user encryption key - */ - getEncryptionKey(): string | null { - return this.userEncryptionKey; - } - - /** - * Get current user ID - */ - getUserId(): string | null { - return this.userId; - } -} - -// Singleton instance -let dbServiceInstance: DatabaseService | null = null; - -export function getDatabaseService(): DatabaseService { - if (!dbServiceInstance) { - dbServiceInstance = new DatabaseService(); - } - return dbServiceInstance; -} diff --git a/electron/database/encryption.ts b/electron/database/encryption.ts deleted file mode 100644 index 968f669..0000000 --- a/electron/database/encryption.ts +++ /dev/null @@ -1,99 +0,0 @@ -import crypto from 'crypto'; - -/** - * Encryption utilities using AES-256-CBC for local database encryption - * EXACTEMENT comme dans Fastify System.ts - */ - -const ALGORITHM = 'aes-256-cbc'; -const KEY_LENGTH = 32; // 256 bits -const IV_LENGTH = 16; // 128 bits -const SALT_LENGTH = 64; - -/** - * Generate a unique encryption key for a user - * This key is generated once at first login and stored securely in electron-store - * @param userId - The user's unique identifier - * @returns Base64 encoded encryption key - */ -export function generateUserEncryptionKey(userId: string): string { - // Generate a random salt for this user - const salt = crypto.randomBytes(SALT_LENGTH); - - // Create a deterministic key based on userId and random salt - // This ensures each user has a unique, strong key - const key = crypto.pbkdf2Sync( - userId, - salt, - 100000, // iterations - KEY_LENGTH, - 'sha512' - ); - - // Combine salt and key for storage - const combined = Buffer.concat([salt, key]); - return combined.toString('base64'); -} - -/** - * Extract the actual encryption key from the stored combined salt+key - * @param storedKey - Base64 encoded salt+key combination - * @returns Encryption key buffer - */ -function extractKeyFromStored(storedKey: string): Buffer { - const combined = Buffer.from(storedKey, 'base64'); - // Extract key (last KEY_LENGTH bytes) - return combined.subarray(SALT_LENGTH, SALT_LENGTH + KEY_LENGTH); -} - -/** - * Encrypt data with user key - EXACTEMENT comme Fastify - * @param data - Data to encrypt - * @param userKey - User's encryption key - * @returns Encrypted string with format "iv:encryptedData" - */ -export function encryptDataWithUserKey(data: string, userKey: string): string { - const key = extractKeyFromStored(userKey); - const iv = crypto.randomBytes(IV_LENGTH); - const cipher = crypto.createCipheriv(ALGORITHM, key, iv); - let encryptedData = cipher.update(data, 'utf8', 'hex'); - encryptedData += cipher.final('hex'); - return iv.toString('hex') + ':' + encryptedData; -} - -/** - * Decrypt data with user key - EXACTEMENT comme Fastify - * @param encryptedData - Encrypted string with format "iv:encryptedData" - * @param userKey - User's encryption key - * @returns Decrypted data - */ -export function decryptDataWithUserKey(encryptedData: string, userKey: string): string { - const [ivHex, encryptedHex] = encryptedData.split(':'); - const iv = Buffer.from(ivHex, 'hex'); - const key = extractKeyFromStored(userKey); - const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); - let decryptedData = decipher.update(encryptedHex, 'hex', 'utf8'); - decryptedData += decipher.final('utf8'); - return decryptedData || ''; -} - -/** - * Hash data using SHA-256 (for non-reversible hashing like titles) - * @param data - Data to hash - * @returns Hex encoded hash - */ -export function hashElement(data: string): string { - return crypto.createHash('sha256').update(data.toLowerCase().trim()).digest('hex'); -} - -// Pour compatibilité avec l'ancien code -export const encrypt = encryptDataWithUserKey; -export const decrypt = decryptDataWithUserKey; -export const hash = hashElement; - -// Interface pour compatibilité (pas utilisée avec AES-CBC) -export interface EncryptedData { - encryptedData: string; - iv: string; - authTag: string; -} \ No newline at end of file diff --git a/electron/database/keyManager.ts b/electron/database/keyManager.ts deleted file mode 100644 index f8a6195..0000000 --- a/electron/database/keyManager.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getSecureStorage } from '../storage/SecureStorage.js'; - -/** - * Key Manager - Manages user encryption keys using OS-level secure storage - * - macOS: Keychain - * - Windows: DPAPI - * - Linux: gnome-libsecret/kwallet - */ - -/** - * Get user encryption key from secure storage - * @param userId - User ID - * @returns User's encryption key - * @throws Error if encryption key not found - */ -export function getUserEncryptionKey(userId: string): string { - const storage = getSecureStorage(); - const key = storage.get(`encryptionKey-${userId}`); - if (key === null || key === undefined) { - throw new Error(`Unknown encryptionKey`); - } - return key; -} - -/** - * Set user encryption key in secure storage (OS-encrypted) - * @param userId - User ID - * @param encryptionKey - Encryption key to store - */ -export function setUserEncryptionKey(userId: string, encryptionKey: string): void { - const storage = getSecureStorage(); - storage.set(`encryptionKey-${userId}`, encryptionKey); -} - -/** - * Check if user has an encryption key - * @param userId - User ID - * @returns True if key exists - */ -export function hasUserEncryptionKey(userId: string): boolean { - const storage = getSecureStorage(); - return storage.has(`encryptionKey-${userId}`); -} - -/** - * Delete user encryption key - * @param userId - User ID - */ -export function deleteUserEncryptionKey(userId: string): void { - const storage = getSecureStorage(); - storage.delete(`encryptionKey-${userId}`); -} diff --git a/electron/database/models/Act.ts b/electron/database/models/Act.ts deleted file mode 100644 index 07fb590..0000000 --- a/electron/database/models/Act.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import PlotPoint, { PlotPointProps, PlotPointStory } from "./PlotPoint.js"; -import Incident, { IncidentProps, IncidentStory } from "./Incident.js"; -import ActRepository, { ActQuery } from "../repositories/act.repository.js"; -import Chapter, { ChapterProps } from "./Chapter.js"; -import IncidentRepository from "../repositories/incident.repository.js"; -import PlotPointRepository from "../repositories/plotpoint.repository.js"; -import ChapterRepo from "../repositories/chapter.repository.js"; - -export interface ActProps { - id: number; - summary: string | null; - incidents?: IncidentProps[]; - plotPoints?: PlotPointProps[]; - chapters?: ActChapter[]; -} - -export interface ActStory { - actId: number; - summary: string; - chapterSummary: string; - chapterGoal: string; - incidents: IncidentStory[]; - plotPoints: PlotPointStory[]; -} - -export interface ActChapter { - chapterInfoId: number; - chapterId: string; - title: string; - chapterOrder: number; - actId: number; - incidentId: string | null; - plotPointId: string | null; - summary: string; - goal: string; -} - -export interface SyncedActSummary { - id: string; - lastUpdate: number; -} - -export default class Act { - /** - * Retrieves all acts data for a specific book, including chapters, incidents, and plot points. - * Decrypts summaries using the user's encryption key. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param lang - The language for localization ('fr' or 'en'), defaults to 'fr' - * @returns A promise resolving to an array of Act objects with their associated data - */ - public static async getActsData(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): Promise { - const userEncryptionKey: string = getUserEncryptionKey(userId); - const actChapters: ActChapter[] = Chapter.getAllChapterFromActs(userId, bookId, lang); - const actQueries: ActQuery[] = ActRepository.fetchAllActs(userId, bookId, lang); - const bookIncidents: IncidentProps[] = await Incident.getIncitentsIncidents(userId, bookId, actChapters); - const bookPlotPoints: PlotPointProps[] = await PlotPoint.getPlotPoints(userId, bookId, actChapters); - - const acts: ActProps[] = []; - - acts.push({ - id: 1, - summary: '', - chapters: actChapters.filter((chapter: ActChapter) => chapter.actId === 1) - }); - - acts.push({ - id: 2, - summary: '', - incidents: bookIncidents ? bookIncidents : [], - }); - - acts.push({ - id: 3, - summary: '', - plotPoints: bookPlotPoints ? bookPlotPoints : [], - }); - - acts.push({ - id: 4, - summary: '', - chapters: actChapters.filter((chapter: ActChapter) => chapter.actId === 4) - }); - - acts.push({ - id: 5, - summary: '', - chapters: actChapters.filter((chapter: ActChapter) => chapter.actId === 5) - }); - - if (actQueries.length > 0) { - for (const actQuery of actQueries) { - acts[actQuery.act_index - 1].summary = actQuery.summary && userEncryptionKey - ? System.decryptDataWithUserKey(actQuery.summary, userEncryptionKey) - : ''; - } - } - - return acts; - } - - /** - * Updates multiple acts including their summaries, incidents, plot points, and chapter information. - * Encrypts all sensitive data before storing in the database. - * @param acts - Array of act properties to update - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param userKey - The user's encryption key for data encryption - * @param lang - The language for localization ('fr' or 'en'), defaults to 'fr' - * @returns A promise resolving to true when all updates are complete - */ - public static async updateAct(acts: ActProps[], userId: string, bookId: string, userKey: string, lang: 'fr' | 'en' = 'fr'): Promise { - for (const act of acts) { - const actIncidents: IncidentProps[] = act.incidents ? act.incidents : []; - const actId: number = act.id; - - if (actId === 1 || actId === 4 || actId === 5) { - const encryptedActSummary: string = act.summary ? System.encryptDataWithUserKey(act.summary, userKey) : ''; - try { - ActRepository.updateActSummary(userId, bookId, actId, encryptedActSummary, System.timeStampInSeconds(), lang); - } catch (error: unknown) { - const newActSummaryId: string = System.createUniqueId(); - ActRepository.insertActSummary(newActSummaryId, userId, bookId, actId, encryptedActSummary, lang); - } - if (act.chapters) { - Chapter.updateChapterInfos(act.chapters, userId, actId, bookId, null, null, lang); - } - } else if (actId === 2) { - for (const incident of actIncidents) { - const encryptedIncidentSummary: string = incident.summary ? System.encryptDataWithUserKey(incident.summary, userKey) : ''; - const incidentId: string = incident.incidentId; - const incidentTitle: string = incident.title; - const hashedIncidentTitle: string = System.hashElement(incidentTitle); - const encryptedIncidentTitle: string = System.encryptDataWithUserKey(incidentTitle, userKey); - IncidentRepository.updateIncident(userId, bookId, incidentId, encryptedIncidentTitle, hashedIncidentTitle, encryptedIncidentSummary, System.timeStampInSeconds(), lang); - if (incident.chapters) { - Chapter.updateChapterInfos(incident.chapters, userId, actId, bookId, incidentId, null, lang); - } - } - } else { - const actPlotPoints: PlotPointProps[] = act.plotPoints ? act.plotPoints : []; - for (const plotPoint of actPlotPoints) { - const encryptedPlotPointSummary: string = plotPoint.summary ? System.encryptDataWithUserKey(plotPoint.summary, userKey) : ''; - const plotPointId: string = plotPoint.plotPointId; - const plotPointTitle: string = plotPoint.title; - const hashedPlotPointTitle: string = System.hashElement(plotPointTitle); - const encryptedPlotPointTitle: string = System.encryptDataWithUserKey(plotPointTitle, userKey); - PlotPointRepository.updatePlotPoint(userId, bookId, plotPointId, encryptedPlotPointTitle, hashedPlotPointTitle, encryptedPlotPointSummary, System.timeStampInSeconds(), lang); - if (plotPoint.chapters) { - Chapter.updateChapterInfos(plotPoint.chapters, userId, actId, bookId, null, plotPointId, lang); - } - } - } - } - return true; - } - - /** - * Updates the story structure including acts and main chapters. - * Encrypts chapter titles and updates their order in the database. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param acts - Array of act properties to update - * @param mainChapters - Array of main chapter properties to update - * @param lang - The language for localization ('fr' or 'en'), defaults to 'fr' - * @returns True when all updates are complete - */ - public static updateStory(userId: string, bookId: string, acts: ActProps[], mainChapters: ChapterProps[], lang: 'fr' | 'en' = 'fr'): boolean { - const userEncryptionKey: string = getUserEncryptionKey(userId); - Act.updateAct(acts, userId, bookId, userEncryptionKey, lang).then(); - - for (const chapter of mainChapters) { - const chapterId: string = chapter.chapterId; - const chapterTitle: string = chapter.title; - const chapterOrder: number = chapter.chapterOrder; - Chapter.updateChapter(userId, chapterId, chapterTitle, chapterOrder, lang); - } - - return true; - } -} diff --git a/electron/database/models/Book.ts b/electron/database/models/Book.ts deleted file mode 100644 index d96d0dc..0000000 --- a/electron/database/models/Book.ts +++ /dev/null @@ -1,730 +0,0 @@ -import System from '../System.js'; -import { getUserEncryptionKey } from '../keyManager.js'; -import BookRepo, { BookQuery, BookToolsTable, EritBooksTable } from "../repositories/book.repository.js"; -import { BookActSummariesTable } from "../repositories/act.repository.js"; -import { BookAIGuideLineTable, BookGuideLineTable } from "../repositories/guideline.repository.js"; -import ChapterRepo, { - BookChapterInfosTable, - BookChaptersTable, - ChapterBookResult -} from "../repositories/chapter.repository.js"; -import { BookChapterContentTable } from "../repositories/chaptercontent.repository.js"; -import { - BookCharactersAttributesTable, - BookCharactersTable -} from "../repositories/character.repository.js"; -import { BookIncidentsTable } from "../repositories/incident.repository.js"; -import { BookIssuesTable } from "../repositories/issue.repository.js"; -import { - BookLocationTable, - LocationElementTable, - LocationSubElementTable -} 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"; -import { SyncedWorld } from "./World.js"; -import { SyncedIncident } from "./Incident.js"; -import { SyncedPlotPoint } from "./PlotPoint.js"; -import { SyncedIssue } from "./Issue.js"; -import { SyncedActSummary } from "./Act.js"; -import { SyncedAIGuideLine, SyncedGuideLine } from "./GuideLine.js"; -import Cover from "./Cover.js"; -import UserRepo from "../repositories/user.repository.js"; -import RemovedItem from "./RemovedItem.js"; - -export interface SyncedBookTools { - lastUpdate: number; - charactersEnabled: boolean; - worldsEnabled: boolean; - locationsEnabled: boolean; - spellsEnabled: boolean; -} - -export interface BookToolsSettings { - characters: boolean; - worlds: boolean; - locations: boolean; - spells: boolean; -} - -export interface BookProps { - bookId: string; - type: string; - authorId: string; - title: string; - subTitle?: string; - summary?: string; - serieId?: number | null; - seriesId?: string | null; - desiredReleaseDate?: string | null; - desiredWordCount?: number | null; - wordCount?: number; - coverImage?: string | null; - bookMeta?: string; - tools?: BookToolsSettings; -} - -export interface CompleteBook { - eritBooks: EritBooksTable[]; - actSummaries: BookActSummariesTable[]; - aiGuideLine: BookAIGuideLineTable[]; - chapters: BookChaptersTable[]; - chapterContents: BookChapterContentTable[]; - chapterInfos: BookChapterInfosTable[]; - characters: BookCharactersTable[]; - characterAttributes: BookCharactersAttributesTable[]; - guideLine: BookGuideLineTable[]; - incidents: BookIncidentsTable[]; - issues: BookIssuesTable[]; - locations: BookLocationTable[]; - plotPoints: BookPlotPointsTable[]; - worlds: BookWorldTable[]; - worldElements: BookWorldElementsTable[]; - locationElements: LocationElementTable[]; - locationSubElements: LocationSubElementTable[]; - bookTools: BookToolsTable[]; - spells: BookSpellsTable[]; - spellTags: BookSpellTagsTable[]; -} - -export interface SyncedBook { - id: string; - type: string; - title: string; - subTitle: string | null; - lastUpdate: number; - chapters: SyncedChapter[]; - characters: SyncedCharacter[]; - locations: SyncedLocation[]; - worlds: SyncedWorld[]; - incidents: SyncedIncident[]; - plotPoints: SyncedPlotPoint[]; - issues: SyncedIssue[]; - actSummaries: SyncedActSummary[]; - guideLine: SyncedGuideLine | null; - aiGuideLine: SyncedAIGuideLine | null; - bookTools: SyncedBookTools | null; - spells: SyncedSpell[]; - spellTags: SyncedSpellTag[]; -} - -export interface BookSyncCompare { - id: string; - chapters: string[]; - chapterContents: string[]; - chapterInfos: string[]; - characters: string[]; - characterAttributes: string[]; - locations: string[]; - locationElements: string[]; - locationSubElements: string[]; - worlds: string[]; - worldElements: string[]; - incidents: string[]; - plotPoints: string[]; - issues: string[]; - actSummaries: string[]; - guideLine: boolean; - aiGuideLine: boolean; - bookTools: boolean; - spells: string[]; - spellTags: string[]; -} - -export interface CompleteBookData { - bookId: string; - title: string; - subTitle: string; - summary: string; - coverImage: string; - userInfos: { - firstName: string; - lastName: string; - authorName: string; - }, - 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; - description: string | null; - 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; - private authorId: string; - private title: string; - private subTitle: string; - private summary: string; - private serieId: number; - private desiredReleaseDate: string; - private desiredWordCount: number; - private wordCount: number; - private cover: string; - - /** - * Creates a new Book instance. - * @param id - The unique identifier of the book - * @param authorId - The unique identifier of the author (optional) - */ - constructor(id: string, authorId?: string) { - this.id = id; - if (authorId) { - this.authorId = authorId; - } else { - this.authorId = ''; - } - this.title = ''; - this.subTitle = ''; - this.summary = ''; - this.serieId = 0; - this.desiredReleaseDate = ''; - this.desiredWordCount = 0; - this.wordCount = 0; - this.cover = ''; - this.type = ''; - } - - /** - * Retrieves all books for a specific user. - * @param userId - The unique identifier of the user - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of book properties - * @throws Error if the user encryption key is not found - */ - public static async getBooks(userId: string, lang: 'fr' | 'en' = 'fr'): Promise { - const userKey: string | null = getUserEncryptionKey(userId); - if (!userKey) { - throw new Error( - lang === 'fr' ? "Clé d'encryption utilisateur non trouvée." : 'User encryption key not found.' - ); - } - - const books: BookQuery[] = BookRepo.fetchBooks(userId, lang); - if (!books || books.length === 0) { - return []; - } - - return await Promise.all( - books.map(async (book: BookQuery): Promise => { - return { - bookId: book.book_id, - type: book.type, - authorId: book.author_id, - title: System.decryptDataWithUserKey(book.title, userKey), - subTitle: book.sub_title ? System.decryptDataWithUserKey(book.sub_title, userKey) : '', - summary: book.summary ? System.decryptDataWithUserKey(book.summary, userKey) : '', - serieId: book.serie_id || 0, - desiredReleaseDate: book.desired_release_date || '', - desiredWordCount: book.desired_word_count || 0, - wordCount: book.words_count || 0, - coverImage: book.cover_image ? Cover.getPicture(userId, userKey, book.cover_image) : '', - }; - }) ?? [] - ); - } - - /** - * Adds a new book to the database. - * @param bookId - The unique identifier for the book (optional, will be generated if null) - * @param userId - The unique identifier of the user - * @param title - The title of the book - * @param subTitle - The subtitle of the book - * @param summary - The summary of the book - * @param type - The type/genre of the book - * @param serie - The series identifier - * @param publicationDate - The desired publication date - * @param desiredWordCount - The target word count - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to the book ID - * @throws Error if a book with the same title already exists - */ - public static async addBook(bookId: string | null, userId: string, title: string, subTitle: string, summary: string, type: string, serie: number, publicationDate: string, desiredWordCount: number, lang: 'fr' | 'en' = 'fr'): Promise { - let newBookId: string = ''; - const userKey: string | null = getUserEncryptionKey(userId); - - const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey); - const encryptedSubTitle: string = subTitle ? System.encryptDataWithUserKey(subTitle, userKey) : ''; - const encryptedSummary: string = summary ? System.encryptDataWithUserKey(summary, userKey) : ''; - const hashedTitle: string = System.hashElement(title); - const hashedSubTitle: string = subTitle ? System.hashElement(subTitle) : ''; - - if (BookRepo.verifyBookExist(hashedTitle, hashedSubTitle, userId, lang)) { - throw new Error(lang === "fr" ? `Tu as déjà un livre intitulé ${title} - ${subTitle}.` : `You already have a book named ${title} - ${subTitle}.`); - } - if (bookId) { - newBookId = bookId; - } else { - newBookId = System.createUniqueId(); - } - return BookRepo.insertBook(newBookId, userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, type, serie, publicationDate, desiredWordCount, lang); - } - - /** - * Retrieves a single book by its ID. - * @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 A promise resolving to the book properties - */ - public static async getBook(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { - const book: Book = new Book(bookId); - book.getBookInfos(userId); - const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang); - // Récupérer le seriesId depuis series_books - const seriesId: string | null = BookRepo.fetchBookSeriesId(bookId, lang); - return { - bookId: book.getId(), - type: book.getType(), - authorId: book.getAuthorId(), - title: book.getTitle(), - subTitle: book.getSubTitle(), - summary: book.getSummary(), - serieId: book.getSerieId(), - seriesId: seriesId, - desiredReleaseDate: book.getDesiredReleaseDate(), - desiredWordCount: book.getDesiredWordCount(), - wordCount: book.getWordCount(), - coverImage: book.getCover(), - tools: { - characters: bookTools ? bookTools.characters_enabled === 1 : false, - worlds: bookTools ? bookTools.worlds_enabled === 1 : false, - locations: bookTools ? bookTools.locations_enabled === 1 : false, - spells: bookTools ? bookTools.spells_enabled === 1 : false - } - }; - } - - /** - * Updates basic information for a book. - * @param userId - The unique identifier of the user - * @param title - The new title - * @param subTitle - The new subtitle - * @param summary - The new summary - * @param publicationDate - The new publication date - * @param wordCount - The new word count - * @param bookId - The unique identifier of the book - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the update was successful, false otherwise - */ - static updateBookBasicInformation(userId: string, title: string, subTitle: string, summary: string, publicationDate: string, wordCount: number, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean { - const userKey: string = getUserEncryptionKey(userId); - const encryptedTitle: string = System.encryptDataWithUserKey(title, userKey); - const encryptedSubTitle: string = subTitle ? System.encryptDataWithUserKey(subTitle, userKey) : ''; - const encryptedSummary: string = summary ? System.encryptDataWithUserKey(summary, userKey) : ''; - const hashedTitle: string = System.hashElement(title); - const hashedSubTitle: string = subTitle ? System.hashElement(subTitle) : ''; - return BookRepo.updateBookBasicInformation(userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, publicationDate, wordCount, bookId, lang); - } - - /** - * Removes a book from the database. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book to remove - * @param deletedAt - The timestamp of deletion (from UI via System.timeStampInSeconds()) - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the book was removed, false otherwise - */ - public static removeBook(userId: string, bookId: string, deletedAt: number, lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = BookRepo.deleteBook(userId, bookId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'erit_books', bookId, deletedAt, lang); - } - return deleted; - } - - 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); - } - - /** - * Gets the book ID. - * @returns The book's unique identifier - */ - public getId(): string { - return this.id; - } - - /** - * Gets the author ID. - * @returns The author's unique identifier - */ - public getAuthorId(): string { - return this.authorId; - } - - /** - * Gets the book title. - * @returns The decrypted book title - */ - public getTitle(): string { - return this.title; - } - - /** - * Gets the book subtitle. - * @returns The decrypted book subtitle - */ - public getSubTitle(): string { - return this.subTitle; - } - - /** - * Gets the book summary. - * @returns The decrypted book summary - */ - public getSummary(): string { - return this.summary; - } - - /** - * Gets the series ID. - * @returns The series identifier - */ - public getSerieId(): number { - return this.serieId; - } - - /** - * Gets the desired release date. - * @returns The desired release date string - */ - public getDesiredReleaseDate(): string { - return this.desiredReleaseDate; - } - - /** - * Gets the desired word count. - * @returns The target word count - */ - public getDesiredWordCount(): number { - return this.desiredWordCount; - } - - /** - * Gets the current word count. - * @returns The current word count - */ - public getWordCount(): number { - return this.wordCount; - } - - /** - * Gets the cover image. - * @returns The cover image data - */ - public getCover(): string { - return this.cover; - } - - /** - * Gets the book type. - * @returns The book type/genre - */ - public getType(): string { - return this.type; - } - - /** - * Retrieves complete book data including chapters and user information. - * @param userId - The unique identifier of the user - * @param id - The unique identifier of the book - * @param lang - The language for error messages ('fr' or 'en') - * @returns The complete book data with decrypted content - */ - static completeBookData(userId: string, id: string, lang: 'fr' | 'en' = 'fr'): CompleteBookData { - const bookData: BookQuery = BookRepo.fetchBook(id, userId, lang); - const chapters: ChapterBookResult[] = ChapterRepo.fetchCompleteBookChapters(id, lang); - const userKey: string = getUserEncryptionKey(userId); - const userInfos = UserRepo.fetchAccountInformation(userId, lang); - - const bookTitle: string = bookData.title ? System.decryptDataWithUserKey(bookData.title, userKey) : ''; - const decryptedChapters: CompleteChapterContent[] = []; - - for (const chapter of chapters) { - decryptedChapters.push({ - id: '', - title: chapter.title ? System.decryptDataWithUserKey(chapter.title, userKey) : '', - content: chapter.content ? System.decryptDataWithUserKey(chapter.content, userKey) : '', - order: chapter.chapter_order - }) - } - const coverImage: string = bookData.cover_image ? Cover.getPicture(userId, userKey, bookData.cover_image, lang) : ''; - return { - bookId: id, - title: bookTitle, - subTitle: bookData.sub_title ? System.decryptDataWithUserKey(bookData.sub_title, userKey) : '', - summary: bookData.summary ? System.decryptDataWithUserKey(bookData.summary, userKey) : '', - coverImage: coverImage, - userInfos: { - firstName: userInfos.first_name ? System.decryptDataWithUserKey(userInfos.first_name, userKey) : '', - lastName: userInfos.last_name ? System.decryptDataWithUserKey(userInfos.last_name, userKey) : '', - authorName: userInfos.author_name ? System.decryptDataWithUserKey(userInfos.author_name, userKey) : '', - }, - chapters: decryptedChapters - }; - } - - /** - * Loads book information from the database into the instance. - * @param userId - The unique identifier of the user - * @param lang - The language for error messages ('fr' or 'en') - */ - public getBookInfos(userId: string, lang: 'fr' | 'en' = 'fr'): void { - const bookData: BookQuery = BookRepo.fetchBook(this.id, userId, lang); - const userKey: string = getUserEncryptionKey(userId); - if (bookData) { - this.authorId = bookData.author_id; - this.type = bookData.type; - this.title = bookData.title ? System.decryptDataWithUserKey(bookData.title, userKey) : ''; - this.subTitle = bookData.sub_title ? System.decryptDataWithUserKey(bookData.sub_title, userKey) : ''; - this.summary = bookData.summary ? System.decryptDataWithUserKey(bookData.summary, userKey) : ''; - this.serieId = bookData.serie_id ?? 0; - this.desiredReleaseDate = bookData.desired_release_date ?? ''; - this.desiredWordCount = bookData.desired_word_count ?? 0; - this.wordCount = bookData.words_count ?? 0; - this.cover = bookData.cover_image ? Cover.getPicture(userId, userKey, bookData.cover_image, lang) : ''; - } else { - this.authorId = ''; - this.title = ''; - this.subTitle = ''; - this.summary = ''; - this.serieId = 0; - this.desiredReleaseDate = ''; - this.wordCount = 0; - this.cover = ''; - } - } -} diff --git a/electron/database/models/Chapter.ts b/electron/database/models/Chapter.ts deleted file mode 100644 index 4de06eb..0000000 --- a/electron/database/models/Chapter.ts +++ /dev/null @@ -1,664 +0,0 @@ -import System from "../System.js"; -import { getUserEncryptionKey } from "../keyManager.js"; -import Book, { CompleteBookData } from "./Book.js"; -import ChapterRepo, { - ActChapterQuery, - ChapterExportInfoResult, - ChapterQueryResult, - ChapterSelectionParam, - ChapterStoryQueryResult, - LastChapterResult, - SelectedChapterContentResult -} from "../repositories/chapter.repository.js"; -import { ActChapter, ActStory } from "./Act.js"; -import ChapterContentRepository, { - ChapterContentQueryResult, - CompanionContentQueryResult, - ContentQueryResult -} from "../repositories/chaptercontent.repository.js"; -import RemovedItem from "./RemovedItem.js"; - -export interface ChapterContent { - version: number; - content: string; - wordsCount: number; -} - -export interface ChapterContentData extends ChapterContent { - title: string; - chapterOrder: number; -} - -export interface ChapterProps { - chapterId: string; - title: string; - chapterOrder: number; - chapterContent?: ChapterContent -} - -export interface CompanionContent { - version: number; - content: string; - wordsCount: number; -} - -export interface SyncedChapter { - id: string; - name: string; - lastUpdate: number; - contents: SyncedChapterContent[]; - info: SyncedChapterInfo | null; -} - -export interface SyncedChapterContent { - id: string; - lastUpdate: number; -} - -export interface SyncedChapterInfo { - id: string; - lastUpdate: number; -} - -export interface CompleteChapterContent { - id: string; - title: string; - content: string; - order: number; - version?: number; -} - -export interface ChapterExportInfo { - chapterId: string; - title: string; - chapterOrder: number; - availableVersions: number[]; -} - -interface TipTapNode { - type?: string; - text?: string; - content?: TipTapNode[]; - attrs?: Record; - marks?: TipTapMark[]; -} - -interface TipTapMark { - type: string; - attrs?: Record; -} - -export default class Chapter { - /** - * Retrieves all chapters from 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 ChapterProps containing chapter details - */ - public static getAllChaptersFromABook(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps[] { - const chapterQueryResults: ChapterQueryResult[] = ChapterRepo.fetchAllChapterFromABook(userId, bookId, lang); - const decryptedChapters: ChapterProps[] = []; - const userEncryptionKey: string = getUserEncryptionKey(userId); - - for (const chapterResult of chapterQueryResults) { - const decryptedTitle: string = System.decryptDataWithUserKey(chapterResult.title, userEncryptionKey); - decryptedChapters.push({ - chapterId: chapterResult.chapter_id, - title: decryptedTitle, - chapterOrder: chapterResult.chapter_order - }); - } - - return decryptedChapters; - } - - /** - * Retrieves all chapters organized by acts for a specific book. - * Caches decrypted titles to avoid redundant decryption operations. - * @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 ActChapter containing chapter details with act information - */ - public static getAllChapterFromActs(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ActChapter[] { - const actChapterQueryResults: ActChapterQuery[] = ChapterRepo.fetchAllChapterForActs(userId, bookId, lang); - const actChapters: ActChapter[] = []; - const decryptedTitleCache: { id: string; title: string }[] = []; - const userEncryptionKey: string = getUserEncryptionKey(userId); - - if (actChapterQueryResults.length === 0) { - return []; - } - - for (const chapterQueryResult of actChapterQueryResults) { - let decryptedTitle: string = ''; - const cachedTitleIndex: number = decryptedTitleCache.findIndex( - (cachedItem: { id: string; title: string }) => cachedItem.id === chapterQueryResult.chapter_id - ); - - if (cachedTitleIndex > -1) { - decryptedTitle = decryptedTitleCache[cachedTitleIndex]?.title ?? ''; - } else { - decryptedTitle = System.decryptDataWithUserKey(chapterQueryResult.title, userEncryptionKey); - decryptedTitleCache.push({ id: chapterQueryResult.chapter_id, title: decryptedTitle }); - } - - actChapters.push({ - chapterId: chapterQueryResult.chapter_id, - title: decryptedTitle, - actId: chapterQueryResult.act_id, - chapterInfoId: chapterQueryResult.chapter_info_id, - chapterOrder: chapterQueryResult.chapter_order, - goal: chapterQueryResult.goal ? System.decryptDataWithUserKey(chapterQueryResult.goal, userEncryptionKey) : '', - summary: chapterQueryResult.summary ? System.decryptDataWithUserKey(chapterQueryResult.summary, userEncryptionKey) : '', - incidentId: chapterQueryResult.incident_id, - plotPointId: chapterQueryResult.plot_point_id - }); - } - - return actChapters; - } - - /** - * Retrieves a complete chapter with its content for a specific version. - * Optionally updates the last chapter record for the book. - * @param userId - The unique identifier of the user - * @param chapterId - The unique identifier of the chapter - * @param version - The version number of the chapter content - * @param bookId - Optional book identifier to update last chapter record - * @param lang - The language for error messages ('fr' or 'en') - * @returns ChapterProps containing chapter details and content - */ - public static getWholeChapter(userId: string, chapterId: string, version: number, bookId?: string, lang: 'fr' | 'en' = 'fr'): ChapterProps { - const chapterContentResult: ChapterContentQueryResult = ChapterContentRepository.fetchWholeChapter(userId, chapterId, version, lang); - const userEncryptionKey: string = getUserEncryptionKey(userId); - - if (bookId) { - ChapterRepo.updateLastChapterRecord(userId, bookId, chapterId, version, lang); - } - - return { - chapterId: chapterContentResult.chapter_id, - title: System.decryptDataWithUserKey(chapterContentResult.title, userEncryptionKey), - chapterOrder: chapterContentResult.chapter_order, - chapterContent: { - content: chapterContentResult.content ? System.decryptDataWithUserKey(chapterContentResult.content, userEncryptionKey) : '', - version: version, - wordsCount: chapterContentResult.words_count - } - }; - } - - /** - * Saves the content of a chapter for a specific version. - * Encrypts the content before storing it in the database. - * @param userId - The unique identifier of the user - * @param chapterId - The unique identifier of the chapter - * @param version - The version number of the chapter content - * @param content - The JSON content to save - * @param wordsCount - The word count of the content - * @param currentTime - The current timestamp (unused, actual timestamp is generated) - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the content was saved successfully, false otherwise - */ - public static saveChapterContent(userId: string, chapterId: string, version: number, content: JSON, wordsCount: number, currentTime: number, lang: 'fr' | 'en' = 'fr'): boolean { - const userEncryptionKey: string = getUserEncryptionKey(userId); - const encryptedContent: string = System.encryptDataWithUserKey(JSON.stringify(content), userEncryptionKey); - return ChapterContentRepository.updateChapterContent(userId, chapterId, version, encryptedContent, wordsCount, System.timeStampInSeconds(), lang); - } - - /** - * Retrieves the last accessed chapter for a specific book. - * Falls back to the first chapter content if no last chapter record exists. - * @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 ChapterProps containing chapter details and content, or null if no chapters exist - */ - public static getLastChapter(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterProps | null { - const lastChapterRecord: LastChapterResult | null = ChapterRepo.fetchLastChapter(userId, bookId, lang); - - if (lastChapterRecord) { - return Chapter.getWholeChapter(userId, lastChapterRecord.chapter_id, lastChapterRecord.version, bookId, lang); - } - - const chapterContentResults: ChapterContentQueryResult[] = ChapterContentRepository.fetchLastChapterContent(userId, bookId, lang); - - if (chapterContentResults.length === 0) { - return null; - } - - const firstChapterContent: ChapterContentQueryResult = chapterContentResults[0]; - const userEncryptionKey: string = getUserEncryptionKey(userId); - - return { - chapterId: firstChapterContent.chapter_id, - title: firstChapterContent.title ? System.decryptDataWithUserKey(firstChapterContent.title, userEncryptionKey) : '', - chapterOrder: firstChapterContent.chapter_order, - chapterContent: { - content: firstChapterContent.content ? System.decryptDataWithUserKey(firstChapterContent.content, userEncryptionKey) : '', - version: firstChapterContent.version, - wordsCount: firstChapterContent.words_count - } - }; - } - - /** - * Adds a new chapter to a book. - * Validates that the chapter name is unique within the book. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param title - The title of the new chapter - * @param wordsCount - The initial word count of the chapter - * @param chapterOrder - The order position of the chapter - * @param lang - The language for error messages ('fr' or 'en') - * @param existingChapterId - Optional existing chapter ID for updates - * @returns The unique identifier of the created chapter - * @throws Error if a chapter with the same name already exists - */ - public static addChapter(userId: string, bookId: string, title: string, wordsCount: number, chapterOrder: number, lang: 'fr' | 'en' = 'fr', existingChapterId?: string): string { - const hashedTitle: string = System.hashElement(title); - const userEncryptionKey: string = getUserEncryptionKey(userId); - const encryptedTitle: string = System.encryptDataWithUserKey(title, userEncryptionKey); - - if (!existingChapterId && ChapterRepo.checkNameDuplication(userId, bookId, hashedTitle, lang)) { - throw new Error(lang === 'fr' ? `Ce nom de chapitre existe déjà.` : `This chapter name already exists.`); - } - - const chapterId: string = existingChapterId || System.createUniqueId(); - return ChapterRepo.insertChapter(chapterId, userId, bookId, encryptedTitle, hashedTitle, wordsCount, chapterOrder, lang); - } - - /** - * Removes a chapter from the database. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param chapterId - The unique identifier of the chapter to remove - * @param deletedAt - The timestamp of deletion (from UI via System.timeStampInSeconds()) - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the chapter was removed successfully, false otherwise - */ - public static removeChapter(userId: string, bookId: string, chapterId: string, deletedAt: number, lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = ChapterRepo.deleteChapter(userId, chapterId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'book_chapters', chapterId, deletedAt, lang); - } - return deleted; - } - - /** - * Adds chapter information linking a chapter to an act, plot point, and/or incident. - * @param userId - The unique identifier of the user - * @param chapterId - The unique identifier of the chapter - * @param actId - The act number the chapter belongs to - * @param bookId - The unique identifier of the book - * @param plotId - Optional plot point identifier - * @param incidentId - Optional incident identifier - * @param lang - The language for error messages ('fr' or 'en') - * @param existingChapterInfoId - Optional existing chapter info ID for updates - * @returns The unique identifier of the created chapter information - */ - public static addChapterInformation(userId: string, chapterId: string, actId: number, bookId: string, plotId: string | null, incidentId: string | null, lang: 'fr' | 'en' = 'fr', existingChapterInfoId?: string): string { - const chapterInfoId: string = existingChapterInfoId || System.createUniqueId(); - return ChapterRepo.insertChapterInformation(chapterInfoId, userId, chapterId, actId, bookId, plotId, incidentId, lang); - } - - /** - * Updates a chapter's title and order position. - * @param userId - The unique identifier of the user - * @param chapterId - The unique identifier of the chapter - * @param title - The new title for the chapter - * @param chapterOrder - The new order position for the chapter - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the chapter was updated successfully, false otherwise - */ - public static updateChapter(userId: string, chapterId: string, title: string, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): boolean { - const hashedTitle: string = System.hashElement(title); - const userEncryptionKey: string = getUserEncryptionKey(userId); - const encryptedTitle: string = System.encryptDataWithUserKey(title, userEncryptionKey); - return ChapterRepo.updateChapter(userId, chapterId, encryptedTitle, hashedTitle, chapterOrder, System.timeStampInSeconds(), lang); - } - - /** - * Updates chapter information for multiple chapters including summary and goal. - * @param chapters - Array of ActChapter objects containing updated information - * @param userId - The unique identifier of the user - * @param actId - The act number the chapters belong to - * @param bookId - The unique identifier of the book - * @param incidentId - Optional incident identifier - * @param plotId - Optional plot point identifier - * @param lang - The language for error messages ('fr' or 'en') - */ - static updateChapterInfos(chapters: ActChapter[], userId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, lang: 'fr' | 'en' = 'fr'): void { - const userEncryptionKey: string = getUserEncryptionKey(userId); - - for (const chapterData of chapters) { - const encryptedSummary: string = chapterData.summary ? System.encryptDataWithUserKey(chapterData.summary, userEncryptionKey) : ''; - const encryptedGoal: string = chapterData.goal ? System.encryptDataWithUserKey(chapterData.goal, userEncryptionKey) : ''; - const chapterId: string = chapterData.chapterId; - ChapterRepo.updateChapterInfos(userId, chapterId, actId, bookId, incidentId, plotId, encryptedSummary, encryptedGoal, System.timeStampInSeconds(), lang); - } - } - - /** - * Retrieves the companion content for a chapter (previous version content). - * @param userId - The unique identifier of the user - * @param chapterId - The unique identifier of the chapter - * @param version - The current version number (companion is version - 1) - * @param lang - The language for error messages ('fr' or 'en') - * @returns CompanionContent containing the previous version's content - */ - static getCompanionContent(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): CompanionContent { - const companionVersion: number = version - 1; - const companionContentResults: CompanionContentQueryResult[] = ChapterContentRepository.fetchCompanionContent(userId, chapterId, companionVersion, lang); - - if (companionContentResults.length === 0) { - return { - version: version, - content: '', - wordsCount: 0 - }; - } - - const companionContentData: CompanionContentQueryResult = companionContentResults[0]; - const userEncryptionKey: string = getUserEncryptionKey(userId); - - return { - version: companionContentData.version, - content: companionContentData.content ? System.decryptDataWithUserKey(companionContentData.content, userEncryptionKey) : '', - wordsCount: companionContentData.words_count - }; - } - - /** - * Retrieves the story context for a chapter including act summaries, incidents, and plot points. - * @param userId - The unique identifier of the user - * @param chapterId - The unique identifier of the chapter - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of ActStory containing story context organized by act - */ - static getChapterStory(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): ActStory[] { - const chapterStoryResults: ChapterStoryQueryResult[] = ChapterRepo.fetchChapterStory(userId, chapterId, lang); - const actStoriesMap: Record = {}; - const userEncryptionKey: string = getUserEncryptionKey(userId); - - for (const storyResult of chapterStoryResults) { - const actId: number = storyResult.act_id; - - if (!actStoriesMap[actId]) { - actStoriesMap[actId] = { - actId: actId, - summary: storyResult.summary ? System.decryptDataWithUserKey(storyResult.summary, userEncryptionKey) : '', - chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '', - chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : '', - incidents: [], - plotPoints: [] - }; - } - - if (storyResult.incident_id) { - const decryptedIncidentTitle: string = storyResult.incident_title ? System.decryptDataWithUserKey(storyResult.incident_title, userEncryptionKey) : ''; - const decryptedIncidentSummary: string = storyResult.incident_summary ? System.decryptDataWithUserKey(storyResult.incident_summary, userEncryptionKey) : ''; - - const incidentAlreadyExists: boolean = actStoriesMap[actId].incidents.some( - (existingIncident) => existingIncident.incidentTitle === decryptedIncidentTitle && existingIncident.incidentSummary === decryptedIncidentSummary - ); - - if (!incidentAlreadyExists) { - actStoriesMap[actId].incidents.push({ - incidentTitle: decryptedIncidentTitle, - incidentSummary: decryptedIncidentSummary, - chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '', - chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : '' - }); - } - } - - if (storyResult.plot_point_id) { - const decryptedPlotTitle: string = storyResult.plot_title ? System.decryptDataWithUserKey(storyResult.plot_title, userEncryptionKey) : ''; - const decryptedPlotSummary: string = storyResult.plot_summary ? System.decryptDataWithUserKey(storyResult.plot_summary, userEncryptionKey) : ''; - - const plotPointAlreadyExists: boolean = actStoriesMap[actId].plotPoints.some( - (existingPlotPoint) => existingPlotPoint.plotTitle === decryptedPlotTitle && existingPlotPoint.plotSummary === decryptedPlotSummary - ); - - if (!plotPointAlreadyExists) { - actStoriesMap[actId].plotPoints.push({ - plotTitle: decryptedPlotTitle, - plotSummary: decryptedPlotSummary, - chapterSummary: storyResult.chapter_summary ? System.decryptDataWithUserKey(storyResult.chapter_summary, userEncryptionKey) : '', - chapterGoal: storyResult.chapter_goal ? System.decryptDataWithUserKey(storyResult.chapter_goal, userEncryptionKey) : '' - }); - } - } - } - - return Object.values(actStoriesMap); - } - - /** - * Retrieves the content of a specific chapter version. - * @param userId - The unique identifier of the user - * @param chapterId - The unique identifier of the chapter - * @param version - The version number of the content to retrieve - * @param lang - The language for error messages ('fr' or 'en') - * @returns The decrypted content string, or empty string if not found - */ - static getChapterContentByVersion(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): string { - const contentResult: ContentQueryResult = ChapterContentRepository.fetchChapterContentByVersion(userId, chapterId, version, lang); - const userEncryptionKey: string = getUserEncryptionKey(userId); - return contentResult.content ? System.decryptDataWithUserKey(contentResult.content, userEncryptionKey) : ''; - } - - /** - * Removes chapter information by its identifier. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param chapterInfoId - The unique identifier of the chapter information to remove - * @param deletedAt - The timestamp of deletion (from UI via System.timeStampInSeconds()) - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the chapter information was removed successfully, false otherwise - */ - static removeChapterInformation(userId: string, bookId: string, chapterInfoId: string, deletedAt: number, lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = ChapterRepo.deleteChapterInformation(userId, chapterInfoId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'book_chapter_infos', chapterInfoId, deletedAt, lang); - } - return deleted; - } - - /** - * Converts TipTap JSON content to HTML string. - * Handles various node types including paragraphs, headings, lists, and text marks. - * @param tipTapContent - The TipTap JSON content to convert - * @returns The converted HTML string - */ - static tipTapToHtml(tipTapContent: JSON): string { - const escapeHtmlCharacters = (text: string): string => { - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - }; - - const renderTextWithMarks = (text: string, marks?: TipTapMark[]): string => { - if (!marks || marks.length === 0) return escapeHtmlCharacters(text); - - let renderedText: string = escapeHtmlCharacters(text); - - marks.forEach((mark: TipTapMark) => { - switch (mark.type) { - case 'bold': - renderedText = `${renderedText}`; - break; - case 'italic': - renderedText = `${renderedText}`; - break; - case 'underline': - renderedText = `${renderedText}`; - break; - case 'strike': - renderedText = `${renderedText}`; - break; - case 'code': - renderedText = `${renderedText}`; - break; - case 'link': - const linkHref: string = (mark.attrs?.href as string) || '#'; - renderedText = `${renderedText}`; - break; - } - }); - - return renderedText; - }; - - const renderTipTapNode = (node: TipTapNode): string => { - if (!node) return ''; - - if (node.type === 'text') { - const textContent: string = node.text || '\u00A0'; - return renderTextWithMarks(textContent, node.marks); - } - - const childrenHtml: string = node.content?.map(renderTipTapNode).join('') || ''; - const textAlignStyle: string = node.attrs?.textAlign ? ` style="text-align: ${node.attrs.textAlign}"` : ''; - - switch (node.type) { - case 'doc': - return childrenHtml; - case 'paragraph': - return `${childrenHtml || '\u00A0'}

`; - case 'heading': - const headingLevel: number = (node.attrs?.level as number) || 1; - return `${childrenHtml}`; - case 'bulletList': - return `
    ${childrenHtml}
`; - case 'orderedList': - return `
    ${childrenHtml}
`; - case 'listItem': - return `
  • ${childrenHtml}
  • `; - case 'blockquote': - return `
    ${childrenHtml}
    `; - case 'codeBlock': - return `
    ${childrenHtml}
    `; - case 'hardBreak': - return '
    '; - case 'horizontalRule': - return '
    '; - default: - return childrenHtml; - } - }; - - const contentNode: TipTapNode = tipTapContent as unknown as TipTapNode; - return renderTipTapNode(contentNode); - } - - /** - * Retrieves all chapters with their content data 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 ChapterContentData containing chapter details with content - */ - static getAllChapters(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterContentData[] { - try { - const completeBookData: CompleteBookData = Book.completeBookData(userId, bookId, lang); - return Chapter.getChaptersOrSheet(completeBookData.chapters); - } catch (error: unknown) { - return []; - } - } - - /** - * Processes book chapters to return either sheet content or chapter content. - * If only a sheet exists (order -1), returns the sheet. Otherwise, returns all positive-order chapters. - * @param bookChapters - Array of CompleteChapterContent from the book - * @returns An array of ChapterContentData with processed content - */ - static getChaptersOrSheet(bookChapters: CompleteChapterContent[]): ChapterContentData[] { - const processedChapters: ChapterContentData[] = []; - const sheetContent: CompleteChapterContent | undefined = bookChapters.find( - (chapter: CompleteChapterContent): boolean => chapter.order === -1 - ); - const regularChapter: CompleteChapterContent | undefined = bookChapters.find( - (chapter: CompleteChapterContent): boolean => chapter.order > 0 - ); - - if (sheetContent && !regularChapter) { - processedChapters.push({ - title: sheetContent.title, - chapterOrder: sheetContent.order, - content: System.htmlToText(Chapter.tipTapToHtml(JSON.parse(sheetContent.content))), - wordsCount: 0, - version: sheetContent.version || 0 - }); - } else if (regularChapter) { - for (const chapterData of bookChapters) { - if (chapterData.order < 0) continue; - processedChapters.push({ - title: chapterData.title, - chapterOrder: chapterData.order, - content: System.htmlToText(Chapter.tipTapToHtml(JSON.parse(chapterData.content))), - wordsCount: 0, - version: chapterData.version || 0 - }); - } - } - - return processedChapters; - } - - static getChaptersExportInfo(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterExportInfo[] { - const results: ChapterExportInfoResult[] = ChapterRepo.fetchChaptersExportInfo(userId, bookId, lang); - const userEncryptionKey: string = getUserEncryptionKey(userId); - const exportInfos: ChapterExportInfo[] = []; - - for (const result of results) { - if (!result.available_versions) continue; - const versions: number[] = result.available_versions - .split(',') - .map((v: string): number => parseInt(v, 10)) - .filter((v: number): boolean => !isNaN(v)); - if (versions.length === 0) continue; - exportInfos.push({ - chapterId: result.chapter_id, - title: result.title ? System.decryptDataWithUserKey(result.title, userEncryptionKey) : '', - chapterOrder: result.chapter_order, - availableVersions: versions.sort((a: number, b: number): number => a - b) - }); - } - - return exportInfos; - } - - static getCompleteBookDataWithSelections(userId: string, bookId: string, selections: ChapterSelectionParam[] | null, lang: 'fr' | 'en' = 'fr'): CompleteBookData { - if (!selections || selections.length === 0) { - return Book.completeBookData(userId, bookId, lang); - } - - const bookData: CompleteBookData = Book.completeBookData(userId, bookId, lang); - const selectedResults: SelectedChapterContentResult[] = ChapterRepo.fetchSelectedChaptersContent(bookId, selections, lang); - const userEncryptionKey: string = getUserEncryptionKey(userId); - const selectedChapters: CompleteChapterContent[] = []; - - for (const result of selectedResults) { - selectedChapters.push({ - id: result.chapter_id, - title: result.title ? System.decryptDataWithUserKey(result.title, userEncryptionKey) : '', - content: result.content ? System.decryptDataWithUserKey(result.content, userEncryptionKey) : '', - order: result.chapter_order, - version: result.version - }); - } - - return { - ...bookData, - chapters: selectedChapters - }; - } -} diff --git a/electron/database/models/Character.ts b/electron/database/models/Character.ts deleted file mode 100644 index ad0bf63..0000000 --- a/electron/database/models/Character.ts +++ /dev/null @@ -1,515 +0,0 @@ -import CharacterRepo, { - AttributeResult, - CharacterResult, - CompleteCharacterResult -} from "../repositories/character.repository.js"; -import BookRepo, {BookToolsTable} from "../repositories/book.repository.js"; -import System from "../System.js"; -import {getUserEncryptionKey} from "../keyManager.js"; -import RemovedItem from "./RemovedItem.js"; - -export type CharacterCategory = 'Main' | 'Secondary' | 'Recurring'; - -export interface CharacterPropsPost { - 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; - seriesCharacterId?: string | null; -} - - -export interface CharacterProps { - 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; - seriesCharacterId: string | null; -} - -export interface CharacterListResponse { - characters: CharacterProps[]; - enabled: boolean; -} - -export interface CompleteCharacterProps { - 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; - [key: string]: Attribute[] | string | number | null | undefined; -} - -export interface Attribute { - id: string; - name: string; -} - - -export interface CharacterAttribute { - type: string; - values: Attribute[]; -} - -export interface SyncedCharacter { - id: string; - name: string; - lastUpdate: number; - attributes: SyncedCharacterAttribute[]; -} - -export interface SyncedCharacterAttribute { - id: string; - name: string; - lastUpdate: number; -} - -export default class Character { - /** - * Retrieves a list of all characters for a specific book. - * Decrypts character data using the user's encryption key. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param lang - The language code for localization (defaults to 'fr') - * @returns An array of decrypted character properties - */ - public static getCharacterList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): CharacterListResponse { - const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang); - const enabled: boolean = bookTools ? bookTools.characters_enabled === 1 : false; - - const userEncryptionKey: string = getUserEncryptionKey(userId); - const encryptedCharacters: CharacterResult[] = CharacterRepo.fetchCharacters(userId, bookId, lang); - if (!encryptedCharacters || encryptedCharacters.length === 0) { - return { characters: [], enabled }; - } - const decryptedCharacterList: CharacterProps[] = []; - for (const encryptedCharacter of encryptedCharacters) { - decryptedCharacterList.push({ - id: encryptedCharacter.character_id, - 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 ? 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) : '', - status: encryptedCharacter.status ? System.decryptDataWithUserKey(encryptedCharacter.status, userEncryptionKey) : 'alive', - title: encryptedCharacter.title ? System.decryptDataWithUserKey(encryptedCharacter.title, userEncryptionKey) : '', - category: encryptedCharacter.category ? System.decryptDataWithUserKey(encryptedCharacter.category, userEncryptionKey) : '', - image: encryptedCharacter.image ? System.decryptDataWithUserKey(encryptedCharacter.image, userEncryptionKey) : '', - role: encryptedCharacter.role ? System.decryptDataWithUserKey(encryptedCharacter.role, userEncryptionKey) : '', - biography: encryptedCharacter.biography ? System.decryptDataWithUserKey(encryptedCharacter.biography, userEncryptionKey) : '', - history: encryptedCharacter.history ? System.decryptDataWithUserKey(encryptedCharacter.history, userEncryptionKey) : '', - speechPattern: encryptedCharacter.speech_pattern ? System.decryptDataWithUserKey(encryptedCharacter.speech_pattern, userEncryptionKey) : '', - catchphrase: encryptedCharacter.catchphrase ? System.decryptDataWithUserKey(encryptedCharacter.catchphrase, userEncryptionKey) : '', - 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 }; - } - - /** - * Creates a new character with all its attributes for a specific book. - * Encrypts all character data before storing in the database. - * @param userId - The unique identifier of the user - * @param character - The character data to be created - * @param bookId - The unique identifier of the book - * @param lang - The language code for localization (defaults to 'fr') - * @param existingCharacterId - Optional existing character ID for updates or imports - * @returns The unique identifier of the newly created character - */ - public static addNewCharacter(userId: string, character: CharacterPropsPost, bookId: string, lang: 'fr' | 'en' = 'fr', existingCharacterId?: string): string { - const userEncryptionKey: string = getUserEncryptionKey(userId); - const characterId: string = existingCharacterId || System.createUniqueId(); - - const characterData = { - firstName: System.encryptDataWithUserKey(character.name, userEncryptionKey), - lastName: System.encryptDataWithUserKey(character.lastName, userEncryptionKey), - nickname: System.encryptDataWithUserKey(character.nickname || '', 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), - status: System.encryptDataWithUserKey(character.status || 'alive', userEncryptionKey), - title: System.encryptDataWithUserKey(character.title, userEncryptionKey), - category: System.encryptDataWithUserKey(character.category, userEncryptionKey), - image: System.encryptDataWithUserKey(character.image, userEncryptionKey), - role: System.encryptDataWithUserKey(character.role, userEncryptionKey), - biography: System.encryptDataWithUserKey(character.biography || '', userEncryptionKey), - history: System.encryptDataWithUserKey(character.history || '', userEncryptionKey), - speechPattern: System.encryptDataWithUserKey(character.speechPattern || '', userEncryptionKey), - catchphrase: System.encryptDataWithUserKey(character.catchphrase || '', userEncryptionKey), - residence: System.encryptDataWithUserKey(character.residence || '', userEncryptionKey), - notes: System.encryptDataWithUserKey(character.notes || '', userEncryptionKey), - color: System.encryptDataWithUserKey(character.color || '', userEncryptionKey), - }; - - 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])) { - const attributeArray = character[propertyKey as keyof CharacterPropsPost] as { name: string }[]; - if (attributeArray.length > 0) { - for (const attributeItem of attributeArray) { - const attributeType: string = propertyKey; - const attributeName: string = attributeItem.name; - this.addNewAttribute(characterId, userId, attributeType, attributeName, lang); - } - } - } - } - return characterId; - } - - /** - * Updates an existing character's core properties. - * Encrypts all updated data before storing in the database. - * @param userId - The unique identifier of the user - * @param character - The character data with updated values - * @param lang - The language code for localization (defaults to 'fr') - * @returns True if the update was successful, false otherwise - */ - static updateCharacter(userId: string, character: CharacterPropsPost, lang: 'fr' | 'en' = 'fr'): boolean { - const userEncryptionKey: string = getUserEncryptionKey(userId); - if (!character.id) { - return false; - } - - const characterData = { - firstName: System.encryptDataWithUserKey(character.name, userEncryptionKey), - lastName: System.encryptDataWithUserKey(character.lastName, userEncryptionKey), - nickname: System.encryptDataWithUserKey(character.nickname || '', 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), - status: System.encryptDataWithUserKey(character.status || 'alive', userEncryptionKey), - title: System.encryptDataWithUserKey(character.title, userEncryptionKey), - category: System.encryptDataWithUserKey(character.category, userEncryptionKey), - image: System.encryptDataWithUserKey(character.image, userEncryptionKey), - role: System.encryptDataWithUserKey(character.role, userEncryptionKey), - biography: System.encryptDataWithUserKey(character.biography || '', userEncryptionKey), - history: System.encryptDataWithUserKey(character.history || '', userEncryptionKey), - speechPattern: System.encryptDataWithUserKey(character.speechPattern || '', userEncryptionKey), - catchphrase: System.encryptDataWithUserKey(character.catchphrase || '', userEncryptionKey), - residence: System.encryptDataWithUserKey(character.residence || '', userEncryptionKey), - notes: System.encryptDataWithUserKey(character.notes || '', userEncryptionKey), - color: System.encryptDataWithUserKey(character.color || '', userEncryptionKey), - }; - - return CharacterRepo.updateCharacter(userId, character.id, characterData, System.timeStampInSeconds(), lang, character.seriesCharacterId || null); - } - - /** - * Adds a new attribute to a character. - * Attributes are categorized properties like physical traits, skills, or goals. - * @param characterId - The unique identifier of the character - * @param userId - The unique identifier of the user - * @param type - The type/category of the attribute (e.g., 'physical', 'skills') - * @param name - The value/name of the attribute - * @param lang - The language code for localization (defaults to 'fr') - * @param existingAttributeId - Optional existing attribute ID for updates or imports - * @returns The unique identifier of the newly created attribute - */ - static addNewAttribute(characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr', existingAttributeId?: string): string { - const userEncryptionKey: string = getUserEncryptionKey(userId); - const attributeId: string = existingAttributeId || System.createUniqueId(); - const encryptedType: string = System.encryptDataWithUserKey(type, userEncryptionKey); - const encryptedName: string = System.encryptDataWithUserKey(name, userEncryptionKey); - return CharacterRepo.insertAttribute(attributeId, characterId, userId, encryptedType, encryptedName, lang); - } - - /** - * Deletes an attribute from a character. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param attributeId - The unique identifier of the attribute to delete - * @param deletedAt - The timestamp of deletion - * @param lang - The language code for localization (defaults to 'fr') - * @returns True if the deletion was successful, false otherwise - */ - static deleteAttribute(userId: string, bookId: string, attributeId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = CharacterRepo.deleteAttribute(userId, attributeId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'book_characters_attributes', attributeId, deletedAt, lang); - } - return deleted; - } - - /** - * Deletes a character and all its related data. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param characterId - The unique identifier of the character to delete - * @param deletedAt - The timestamp of deletion - * @param lang - The language code for localization (defaults to 'fr') - * @returns True if the deletion was successful - */ - static deleteCharacter(userId: string, bookId: string, characterId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = CharacterRepo.deleteCharacter(userId, characterId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'book_characters', characterId, deletedAt, lang); - } - return deleted; - } - - /** - * Retrieves all attributes for a specific character, grouped by type. - * Decrypts attribute data using the user's encryption key. - * @param characterId - The unique identifier of the character - * @param userId - The unique identifier of the user - * @param lang - The language code for localization (defaults to 'fr') - * @returns An array of character attributes grouped by type - */ - static getAttributes(characterId: string, userId: string, lang: 'fr' | 'en' = 'fr'): CharacterAttribute[] { - const userEncryptionKey: string = getUserEncryptionKey(userId); - const encryptedAttributes: AttributeResult[] = CharacterRepo.fetchAttributes(characterId, userId, lang); - if (!encryptedAttributes?.length) return []; - - const attributesByType: Map = new Map(); - - for (const encryptedAttribute of encryptedAttributes) { - const decryptedType: string = System.decryptDataWithUserKey(encryptedAttribute.attribute_name, userEncryptionKey); - const decryptedValue: string = encryptedAttribute.attribute_value ? System.decryptDataWithUserKey(encryptedAttribute.attribute_value, userEncryptionKey) : ''; - - if (!attributesByType.has(decryptedType)) { - attributesByType.set(decryptedType, []); - } - - attributesByType.get(decryptedType)!.push({ - id: encryptedAttribute.attr_id, - name: decryptedValue - }); - } - - return Array.from<[string, Attribute[]], CharacterAttribute>( - attributesByType, - ([type, values]: [string, Attribute[]]): CharacterAttribute => ({type, values}) - ); - } - - /** - * Retrieves complete character data including all attributes for multiple characters. - * Used for exporting or displaying full character profiles. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param characters - An array of character IDs to retrieve - * @param lang - The language code for localization (defaults to 'fr') - * @returns An array of complete character objects with all their attributes - */ - static getCompleteCharacterList(userId: string, bookId: string, characters: string[], lang: 'fr' | 'en' = 'fr'): CompleteCharacterProps[] { - const encryptedCharacterList: CompleteCharacterResult[] = CharacterRepo.fetchCompleteCharacters(userId, bookId, characters, lang); - - if (!encryptedCharacterList || encryptedCharacterList.length === 0) { - return []; - } - - const userEncryptionKey: string = getUserEncryptionKey(userId); - const completeCharactersMap = new Map(); - for (const encryptedCharacter of encryptedCharacterList) { - if (!encryptedCharacter.character_id) { - continue; - } - - if (!completeCharactersMap.has(encryptedCharacter.character_id)) { - const decryptedCharacter: CompleteCharacterProps = { - id: '', - 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 ? 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) : '', - status: encryptedCharacter.status ? System.decryptDataWithUserKey(encryptedCharacter.status as string, userEncryptionKey) : 'alive', - title: encryptedCharacter.title ? System.decryptDataWithUserKey(encryptedCharacter.title as string, userEncryptionKey) : '', - category: encryptedCharacter.category ? System.decryptDataWithUserKey(encryptedCharacter.category as string, userEncryptionKey) : '', - role: encryptedCharacter.role ? System.decryptDataWithUserKey(encryptedCharacter.role as string, userEncryptionKey) : '', - biography: encryptedCharacter.biography ? System.decryptDataWithUserKey(encryptedCharacter.biography as string, userEncryptionKey) : '', - history: encryptedCharacter.history ? System.decryptDataWithUserKey(encryptedCharacter.history as string, userEncryptionKey) : '', - speechPattern: encryptedCharacter.speech_pattern ? System.decryptDataWithUserKey(encryptedCharacter.speech_pattern as string, userEncryptionKey) : '', - catchphrase: encryptedCharacter.catchphrase ? System.decryptDataWithUserKey(encryptedCharacter.catchphrase as string, userEncryptionKey) : '', - residence: encryptedCharacter.residence ? System.decryptDataWithUserKey(encryptedCharacter.residence as string, userEncryptionKey) : '', - notes: encryptedCharacter.notes ? System.decryptDataWithUserKey(encryptedCharacter.notes as string, userEncryptionKey) : '', - color: encryptedCharacter.color ? System.decryptDataWithUserKey(encryptedCharacter.color as string, userEncryptionKey) : '', - physical: [], - psychological: [], - relations: [], - skills: [], - weaknesses: [], - strengths: [], - goals: [], - motivations: [], - arc: [], - secrets: [], - fears: [], - flaws: [], - beliefs: [], - conflicts: [], - quotes: [], - distinguishingMarks: [], - items: [], - affiliations: [] - }; - completeCharactersMap.set(encryptedCharacter.character_id, decryptedCharacter); - } - - const characterEntry: CompleteCharacterProps | undefined = completeCharactersMap.get(encryptedCharacter.character_id); - - if (!encryptedCharacter.attribute_name || !characterEntry) { - continue; - } - const decryptedAttributeName: string = System.decryptDataWithUserKey(encryptedCharacter.attribute_name, userEncryptionKey); - const decryptedAttributeValue: string = encryptedCharacter.attribute_value ? System.decryptDataWithUserKey(encryptedCharacter.attribute_value, userEncryptionKey) : ''; - - if (Array.isArray(characterEntry[decryptedAttributeName])) { - characterEntry[decryptedAttributeName].push({ - id: '', - name: decryptedAttributeValue - }); - } - } - return Array.from(completeCharactersMap.values()); - } - - /** - * Generates a formatted vCard-style string representation of characters. - * Useful for AI context or text-based exports. - * @param characters - An array of complete character objects to format - * @returns A formatted string containing all character information - */ - static characterVCard(characters: CompleteCharacterProps[]): string { - const uniqueCharactersMap = new Map(); - let formattedCharactersDescription: string = ''; - - characters.forEach((character: CompleteCharacterProps): void => { - const characterIdentifier: string = character.name || character.id || 'unknown'; - - if (!uniqueCharactersMap.has(characterIdentifier)) { - uniqueCharactersMap.set(characterIdentifier, { - name: character.name, - lastName: character.lastName, - 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, - speechPattern: character.speechPattern, - catchphrase: character.catchphrase, - residence: character.residence, - notes: character.notes, - color: character.color - }); - } - - const aggregatedCharacterData: CompleteCharacterProps = uniqueCharactersMap.get(characterIdentifier)!; - - Object.keys(character).forEach((propertyName: string): void => { - if (Array.isArray(character[propertyName])) { - if (!aggregatedCharacterData[propertyName]) aggregatedCharacterData[propertyName] = []; - (aggregatedCharacterData[propertyName] as Attribute[]).push(...(character[propertyName] as Attribute[])); - } - }); - }); - - formattedCharactersDescription = Array.from(uniqueCharactersMap.values()).map((character: CompleteCharacterProps): string => { - const characterDescriptionLines: string[] = []; - const fullName: string = [character.name, character.lastName].filter(Boolean).join(' '); - if (fullName) characterDescriptionLines.push(`Nom : ${fullName}`); - - (['category', 'title', 'role', 'biography', 'history'] as const).forEach((propertyKey) => { - if (character[propertyKey]) { - characterDescriptionLines.push(`${propertyKey.charAt(0).toUpperCase() + propertyKey.slice(1)} : ${character[propertyKey]}`); - } - }); - - Object.keys(character).forEach((propertyKey: string): void => { - 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(', '); - characterDescriptionLines.push(`${capitalizedPropertyKey} : ${formattedAttributeValues}`); - } - }); - - return characterDescriptionLines.join('\n'); - }).join('\n\n'); - return formattedCharactersDescription; - } - -} diff --git a/electron/database/models/Content.ts b/electron/database/models/Content.ts deleted file mode 100755 index e27479d..0000000 --- a/electron/database/models/Content.ts +++ /dev/null @@ -1,193 +0,0 @@ -/** - * Represents a TipTap editor node structure. - */ -export interface TiptapNode { - type: string; - content?: TiptapNode[]; - text?: string; - attrs?: { - [key: string]: any; - }; -} - -/** - * Utility class for handling TipTap content conversions. - * Provides methods to convert TipTap JSON content to HTML and plain text. - */ -export default class Content { - /** - * Converts TipTap raw JSON string content to plain text. - * First converts to HTML, then strips HTML tags to produce plain text. - * - * @param content - The TipTap JSON string to convert - * @returns The plain text representation of the content - */ - static convertTipTapRawToText(content: string): string { - const htmlContent: string = this.convertTiptapToHTMLFromString(content); - return this.htmlToText(htmlContent); - } - - /** - * Converts HTML string to plain text by removing tags and normalizing whitespace. - * Preserves paragraph structure by converting block elements to newlines. - * - * @param html - The HTML string to convert - * @returns The plain text representation with preserved paragraph structure - */ - static htmlToText(html: string): string { - return html - .replace(//gi, '\n') - .replace(/<\/?(p|h[1-6]|div)(\s+[^>]*)?>/gi, '\n') - .replace(/<\/?[^>]+(>|$)/g, '') - .replace(/(\n\s*){2,}/g, '\n\n') - .replace(/^\s+|\s+$|(?<=\s)\s+/g, '') - .trim(); - } - - /** - * Converts a TipTap JSON string to HTML. - * Parses the JSON string and delegates to the node-based conversion method. - * - * @param jsonString - The TipTap JSON string to convert - * @returns The HTML representation, or empty string if JSON is invalid - */ - static convertTiptapToHTMLFromString(jsonString: string): string { - let tiptapNode: TiptapNode; - try { - tiptapNode = JSON.parse(jsonString); - } catch (error) { - console.error('Invalid JSON string:', error); - return ''; - } - - return this.convertTiptapToHTML(tiptapNode); - } - - /** - * Recursively converts a TipTap node structure to HTML. - * Handles various node types including documents, paragraphs, headings, lists, - * blockquotes, code blocks, and text with formatting attributes. - * - * @param node - The TipTap node to convert - * @returns The HTML representation of the node and its children - */ - static convertTiptapToHTML(node: TiptapNode): string { - let html = ''; - - switch (node.type) { - case 'doc': - if (node.content) { - node.content.forEach((childNode: TiptapNode) => { - html += this.convertTiptapToHTML(childNode); - }); - } - break; - - case 'paragraph': - html += '

    '; - if (node.content) { - node.content.forEach((childNode: TiptapNode) => { - html += this.convertTiptapToHTML(childNode); - }); - } - html += '

    '; - break; - - case 'text': - let formattedText = node.text || ''; - - if (node.attrs) { - if (node.attrs.bold) { - formattedText = `${formattedText}`; - } - if (node.attrs.italic) { - formattedText = `${formattedText}`; - } - if (node.attrs.underline) { - formattedText = `${formattedText}`; - } - if (node.attrs.strike) { - formattedText = `${formattedText}`; - } - if (node.attrs.link) { - formattedText = `${formattedText}`; - } - } - - html += formattedText; - break; - - case 'heading': - const headingLevel = node.attrs?.level || 1; - html += ``; - if (node.content) { - node.content.forEach((childNode: TiptapNode) => { - html += this.convertTiptapToHTML(childNode); - }); - } - html += ``; - break; - - case 'bulletList': - html += '
      '; - if (node.content) { - node.content.forEach((childNode: TiptapNode) => { - html += this.convertTiptapToHTML(childNode); - }); - } - html += '
    '; - break; - - case 'orderedList': - html += '
      '; - if (node.content) { - node.content.forEach((childNode: TiptapNode) => { - html += this.convertTiptapToHTML(childNode); - }); - } - html += '
    '; - break; - - case 'listItem': - html += '
  • '; - if (node.content) { - node.content.forEach((childNode: TiptapNode) => { - html += this.convertTiptapToHTML(childNode); - }); - } - html += '
  • '; - break; - - case 'blockquote': - html += '
    '; - if (node.content) { - node.content.forEach((childNode: TiptapNode) => { - html += this.convertTiptapToHTML(childNode); - }); - } - html += '
    '; - break; - - case 'codeBlock': - html += '
    ';
    -                if (node.content) {
    -                    node.content.forEach((childNode: TiptapNode) => {
    -                        html += this.convertTiptapToHTML(childNode);
    -                    });
    -                }
    -                html += '
    '; - break; - - default: - console.warn(`Unhandled node type: ${node.type}`); - if (node.content) { - node.content.forEach((childNode: TiptapNode) => { - html += this.convertTiptapToHTML(childNode); - }); - } - break; - } - - return html; - } -} diff --git a/electron/database/models/Cover.ts b/electron/database/models/Cover.ts deleted file mode 100644 index f568a5f..0000000 --- a/electron/database/models/Cover.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import BookRepo, { BookCoverQuery } from "../repositories/book.repository.js"; - -/** - * Cover model class for managing book cover images. - * Provides methods to retrieve, decrypt, and delete cover pictures. - */ -export default class Cover { - /** - * Retrieves and decrypts the cover picture 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 decrypted cover image data, or an empty string if not found - */ - public static async getCoverPicture(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): Promise { - const coverQuery: BookCoverQuery = BookRepo.fetchBookCover(userId, bookId, lang); - if (coverQuery) { - const userEncryptionKey: string = getUserEncryptionKey(userId); - return System.decryptDataWithUserKey(coverQuery.cover_image, userEncryptionKey); - } else { - return ''; - } - } - - /** - * Deletes the cover picture association for a specific book. - * Clears the cover image reference in the database. - * @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 True if the cover was successfully deleted, false otherwise - */ - public static async deleteCoverPicture(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): Promise { - const existingCoverName: string = await Cover.getCoverPicture(userId, bookId, lang); - return BookRepo.updateBookCover(bookId, '', userId, lang); - } - - /** - * Retrieves and decrypts a picture file, returning it as a base64-encoded string. - * @param userId - The unique identifier of the user - * @param userKey - The user's encryption key for decrypting the image path - * @param image - The encrypted image file path - * @param lang - The language for error messages ('fr' or 'en') - * @returns The base64-encoded image data, or an empty string if the image cannot be read - */ - public static getPicture(userId: string, userKey: string, image: string, lang: 'fr' | 'en' = 'fr'): string { - if (!image) return ''; - try { - const decryptedFileName: string = System.decryptDataWithUserKey(image, userKey); - const userDirectory: string = path.join(''); - fs.accessSync(userDirectory, fs.constants.R_OK); - const fileData: Buffer = fs.readFileSync(userDirectory); - return fileData.toString('base64'); - } catch (error: unknown) { - return ''; - } - } -} diff --git a/electron/database/models/Download.ts b/electron/database/models/Download.ts deleted file mode 100644 index bf4bd38..0000000 --- a/electron/database/models/Download.ts +++ /dev/null @@ -1,266 +0,0 @@ -import {getUserEncryptionKey} from "../keyManager.js"; -import System from "../System.js"; -import {CompleteBook} from "./Book.js"; -import BookRepo, {EritBooksTable, BookToolsTable} from "../repositories/book.repository.js"; -import ChapterRepo, { - BookChapterInfosTable, - BookChaptersTable -} from "../repositories/chapter.repository.js"; -import IncidentRepository, {BookIncidentsTable} from "../repositories/incident.repository.js"; -import PlotPointRepository, {BookPlotPointsTable} from "../repositories/plotpoint.repository.js"; -import ChapterContentRepository, {BookChapterContentTable} from "../repositories/chaptercontent.repository.js"; -import CharacterRepo, { - BookCharactersAttributesTable, - BookCharactersTable -} from "../repositories/character.repository.js"; -import LocationRepo, { - BookLocationTable, - LocationElementTable, - LocationSubElementTable -} from "../repositories/location.repository.js"; -import WorldRepository, { - BookWorldElementsTable, - BookWorldTable -} from "../repositories/world.repository.js"; -import ActRepository, {BookActSummariesTable} from "../repositories/act.repository.js"; -import GuidelineRepo, { - BookAIGuideLineTable, - 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 { - /** - * Saves a complete book with all its associated data to the local database. - * This method encrypts all sensitive data using the user's encryption key before storing. - * It processes and inserts all book components including chapters, incidents, plot points, - * chapter contents, chapter infos, characters, character attributes, locations, location elements, - * location sub-elements, worlds, world elements, act summaries, AI guidelines, guidelines, and issues. - * - * @param userId - The unique identifier of the user who owns the book - * @param data - The complete book data structure containing all book components to save - * @param lang - The language code for localization ("fr" for French or "en" for English) - * @returns A promise that resolves to true if all data was saved successfully, false otherwise - */ - static async saveCompleteBook(userId: string, data: CompleteBook, lang: "fr" | "en"): Promise { - const userEncryptionKey: string = getUserEncryptionKey(userId); - - const bookData: EritBooksTable = data.eritBooks[0]; - const encryptedBookTitle: string = System.encryptDataWithUserKey(bookData.title, userEncryptionKey); - const encryptedBookSubTitle: string | null = bookData.sub_title ? System.encryptDataWithUserKey(bookData.sub_title, userEncryptionKey) : null; - const encryptedBookSummary: string | null = bookData.summary ? System.encryptDataWithUserKey(bookData.summary, userEncryptionKey) : null; - const encryptedBookCoverImage: string | null = bookData.cover_image ? System.encryptDataWithUserKey(bookData.cover_image, userEncryptionKey) : null; - - const bookInserted: boolean = BookRepo.insertSyncBook( - bookData.book_id, - userId, - bookData.type, - encryptedBookTitle, - bookData.hashed_title, - encryptedBookSubTitle, - bookData.hashed_sub_title, - encryptedBookSummary, - bookData.serie_id, - bookData.desired_release_date, - bookData.desired_word_count, - bookData.words_count, - encryptedBookCoverImage, - bookData.last_update, - lang - ); - if (!bookInserted) return false; - - const chaptersInserted: boolean = data.chapters.every((chapter: BookChaptersTable): boolean => { - const encryptedChapterTitle: string = System.encryptDataWithUserKey(chapter.title, userEncryptionKey); - return ChapterRepo.insertSyncChapter(chapter.chapter_id, chapter.book_id, userId, encryptedChapterTitle, chapter.hashed_title, chapter.words_count, chapter.chapter_order, chapter.last_update, lang); - }); - if (!chaptersInserted) return false; - - const incidentsInserted: boolean = data.incidents.every((incident: BookIncidentsTable): boolean => { - const encryptedIncidentTitle: string = System.encryptDataWithUserKey(incident.title, userEncryptionKey); - const encryptedIncidentSummary: string | null = incident.summary ? System.encryptDataWithUserKey(incident.summary, userEncryptionKey) : null; - return IncidentRepository.insertSyncIncident(incident.incident_id, userId, incident.book_id, encryptedIncidentTitle, incident.hashed_title, encryptedIncidentSummary, incident.last_update, lang); - }); - if (!incidentsInserted) return false; - - const plotPointsInserted: boolean = data.plotPoints.every((plotPoint: BookPlotPointsTable): boolean => { - const encryptedPlotPointTitle: string = System.encryptDataWithUserKey(plotPoint.title, userEncryptionKey); - const encryptedPlotPointSummary: string | null = plotPoint.summary ? System.encryptDataWithUserKey(plotPoint.summary, userEncryptionKey) : null; - return PlotPointRepository.insertSyncPlotPoint(plotPoint.plot_point_id, encryptedPlotPointTitle, plotPoint.hashed_title, encryptedPlotPointSummary, plotPoint.linked_incident_id, userId, plotPoint.book_id, plotPoint.last_update, lang); - }); - if (!plotPointsInserted) return false; - - const chapterContentsInserted: boolean = data.chapterContents.every((chapterContent: BookChapterContentTable): boolean => { - const encryptedChapterContent: string | null = chapterContent.content ? System.encryptDataWithUserKey(JSON.stringify(chapterContent.content), userEncryptionKey) : null; - return ChapterContentRepository.insertSyncChapterContent(chapterContent.content_id, chapterContent.chapter_id, userId, chapterContent.version, encryptedChapterContent, chapterContent.words_count, chapterContent.time_on_it, chapterContent.last_update, lang); - }); - if (!chapterContentsInserted) return false; - - const chapterInfosInserted: boolean = data.chapterInfos.every((chapterInfo: BookChapterInfosTable): boolean => { - const encryptedChapterSummary: string | null = chapterInfo.summary ? System.encryptDataWithUserKey(chapterInfo.summary, userEncryptionKey) : null; - const encryptedChapterGoal: string | null = chapterInfo.goal ? System.encryptDataWithUserKey(chapterInfo.goal, userEncryptionKey) : null; - return ChapterRepo.insertSyncChapterInfo(chapterInfo.chapter_info_id, chapterInfo.chapter_id, chapterInfo.act_id, chapterInfo.incident_id, chapterInfo.plot_point_id, chapterInfo.book_id, userId, encryptedChapterSummary, encryptedChapterGoal, chapterInfo.last_update, lang); - }); - if (!chapterInfosInserted) return false; - - const charactersInserted: boolean = data.characters.every((character: BookCharactersTable): boolean => { - const characterData = { - firstName: System.encryptDataWithUserKey(character.first_name, userEncryptionKey), - lastName: character.last_name ? System.encryptDataWithUserKey(character.last_name, userEncryptionKey) : null, - nickname: character.nickname ? System.encryptDataWithUserKey(character.nickname, userEncryptionKey) : null, - age: character.age ? System.encryptDataWithUserKey(character.age, userEncryptionKey) : null, - gender: character.gender ? System.encryptDataWithUserKey(character.gender, userEncryptionKey) : null, - species: character.species ? System.encryptDataWithUserKey(character.species, userEncryptionKey) : null, - nationality: character.nationality ? System.encryptDataWithUserKey(character.nationality, userEncryptionKey) : null, - status: character.status ? System.encryptDataWithUserKey(character.status, userEncryptionKey) : null, - category: System.encryptDataWithUserKey(character.category, userEncryptionKey), - title: character.title ? System.encryptDataWithUserKey(character.title, userEncryptionKey) : null, - image: character.image ? System.encryptDataWithUserKey(character.image, userEncryptionKey) : null, - role: character.role ? System.encryptDataWithUserKey(character.role, userEncryptionKey) : null, - biography: character.biography ? System.encryptDataWithUserKey(character.biography, userEncryptionKey) : null, - history: character.history ? System.encryptDataWithUserKey(character.history, userEncryptionKey) : null, - speechPattern: character.speech_pattern ? System.encryptDataWithUserKey(character.speech_pattern, userEncryptionKey) : null, - catchphrase: character.catchphrase ? System.encryptDataWithUserKey(character.catchphrase, userEncryptionKey) : null, - residence: character.residence ? System.encryptDataWithUserKey(character.residence, userEncryptionKey) : null, - notes: character.notes ? System.encryptDataWithUserKey(character.notes, userEncryptionKey) : null, - color: character.color ? System.encryptDataWithUserKey(character.color, userEncryptionKey) : null - }; - return CharacterRepo.insertSyncCharacter(character.character_id, character.book_id, userId, characterData, character.last_update, lang); - }); - if (!charactersInserted) return false; - - const characterAttributesInserted: boolean = data.characterAttributes.every((characterAttribute: BookCharactersAttributesTable): boolean => { - const encryptedAttributeName: string = System.encryptDataWithUserKey(characterAttribute.attribute_name, userEncryptionKey); - const encryptedAttributeValue: string = System.encryptDataWithUserKey(characterAttribute.attribute_value, userEncryptionKey); - return CharacterRepo.insertSyncCharacterAttribute(characterAttribute.attr_id, characterAttribute.character_id, userId, encryptedAttributeName, encryptedAttributeValue, characterAttribute.last_update, lang); - }); - if (!characterAttributesInserted) return false; - - const locationsInserted: boolean = data.locations.every((location: BookLocationTable): boolean => { - const encryptedLocationName: string = System.encryptDataWithUserKey(location.loc_name, userEncryptionKey); - return LocationRepo.insertSyncLocation(location.loc_id, location.book_id, userId, encryptedLocationName, location.loc_original_name, location.last_update, lang); - }); - if (!locationsInserted) return false; - - const locationElementsInserted: boolean = data.locationElements.every((locationElement: LocationElementTable): boolean => { - const encryptedLocationElementName: string = System.encryptDataWithUserKey(locationElement.element_name, userEncryptionKey); - const encryptedLocationElementDescription: string | null = locationElement.element_description ? System.encryptDataWithUserKey(locationElement.element_description, userEncryptionKey) : null; - return LocationRepo.insertSyncLocationElement(locationElement.element_id, locationElement.location, userId, encryptedLocationElementName, locationElement.original_name, encryptedLocationElementDescription, locationElement.last_update, lang); - }); - if (!locationElementsInserted) return false; - - const locationSubElementsInserted: boolean = data.locationSubElements.every((locationSubElement: LocationSubElementTable): boolean => { - const encryptedSubElementName: string = System.encryptDataWithUserKey(locationSubElement.sub_elem_name, userEncryptionKey); - const encryptedSubElementDescription: string | null = locationSubElement.sub_elem_description ? System.encryptDataWithUserKey(locationSubElement.sub_elem_description, userEncryptionKey) : null; - return LocationRepo.insertSyncLocationSubElement(locationSubElement.sub_element_id, locationSubElement.element_id, userId, encryptedSubElementName, locationSubElement.original_name, encryptedSubElementDescription, locationSubElement.last_update, lang); - }); - if (!locationSubElementsInserted) return false; - - const worldsInserted: boolean = data.worlds.every((world: BookWorldTable): boolean => { - const encryptedWorldName: string = System.encryptDataWithUserKey(world.name, userEncryptionKey); - const encryptedWorldHistory: string | null = world.history ? System.encryptDataWithUserKey(world.history, userEncryptionKey) : null; - const encryptedWorldPolitics: string | null = world.politics ? System.encryptDataWithUserKey(world.politics, userEncryptionKey) : null; - const encryptedWorldEconomy: string | null = world.economy ? System.encryptDataWithUserKey(world.economy, userEncryptionKey) : null; - const encryptedWorldReligion: string | null = world.religion ? System.encryptDataWithUserKey(world.religion, userEncryptionKey) : null; - const encryptedWorldLanguages: string | null = world.languages ? System.encryptDataWithUserKey(world.languages, userEncryptionKey) : null; - return WorldRepository.insertSyncWorld(world.world_id, encryptedWorldName, world.hashed_name, userId, world.book_id, encryptedWorldHistory, encryptedWorldPolitics, encryptedWorldEconomy, encryptedWorldReligion, encryptedWorldLanguages, world.last_update, lang); - }); - if (!worldsInserted) return false; - - const worldElementsInserted: boolean = data.worldElements.every((worldElement: BookWorldElementsTable): boolean => { - const encryptedWorldElementName: string = System.encryptDataWithUserKey(worldElement.name, userEncryptionKey); - const encryptedWorldElementDescription: string | null = worldElement.description ? System.encryptDataWithUserKey(worldElement.description, userEncryptionKey) : null; - return WorldRepository.insertSyncWorldElement(worldElement.element_id, worldElement.world_id, userId, worldElement.element_type, encryptedWorldElementName, worldElement.original_name, encryptedWorldElementDescription, worldElement.last_update, lang); - }); - if (!worldElementsInserted) return false; - - const actSummariesInserted: boolean = data.actSummaries.every((actSummary: BookActSummariesTable): boolean => { - const encryptedActSummary: string | null = actSummary.summary ? System.encryptDataWithUserKey(actSummary.summary, userEncryptionKey) : null; - return ActRepository.insertSyncActSummary(actSummary.act_sum_id, actSummary.book_id, userId, actSummary.act_index, encryptedActSummary, actSummary.last_update, lang); - }); - if (!actSummariesInserted) return false; - - const aiGuidelinesInserted: boolean = data.aiGuideLine.every((aiGuideline: BookAIGuideLineTable): boolean => { - const encryptedAIGlobalResume: string | null = aiGuideline.global_resume ? System.encryptDataWithUserKey(aiGuideline.global_resume, userEncryptionKey) : null; - const encryptedAIThemes: string | null = aiGuideline.themes ? System.encryptDataWithUserKey(aiGuideline.themes, userEncryptionKey) : null; - const encryptedAITone: string | null = aiGuideline.tone ? System.encryptDataWithUserKey(aiGuideline.tone, userEncryptionKey) : null; - const encryptedAIAtmosphere: string | null = aiGuideline.atmosphere ? System.encryptDataWithUserKey(aiGuideline.atmosphere, userEncryptionKey) : null; - const encryptedAICurrentResume: string | null = aiGuideline.current_resume ? System.encryptDataWithUserKey(aiGuideline.current_resume, userEncryptionKey) : null; - return GuidelineRepo.insertSyncAIGuideLine(userId, aiGuideline.book_id, encryptedAIGlobalResume, encryptedAIThemes, aiGuideline.verbe_tense, aiGuideline.narrative_type, aiGuideline.langue, aiGuideline.dialogue_type, encryptedAITone, encryptedAIAtmosphere, encryptedAICurrentResume, aiGuideline.last_update, lang); - }); - if (!aiGuidelinesInserted) return false; - - const guidelinesInserted: boolean = data.guideLine.every((guideline: BookGuideLineTable): boolean => { - const encryptedGuidelineTone: string | null = guideline.tone ? System.encryptDataWithUserKey(guideline.tone, userEncryptionKey) : null; - const encryptedGuidelineAtmosphere: string | null = guideline.atmosphere ? System.encryptDataWithUserKey(guideline.atmosphere, userEncryptionKey) : null; - const encryptedGuidelineWritingStyle: string | null = guideline.writing_style ? System.encryptDataWithUserKey(guideline.writing_style, userEncryptionKey) : null; - const encryptedGuidelineThemes: string | null = guideline.themes ? System.encryptDataWithUserKey(guideline.themes, userEncryptionKey) : null; - const encryptedGuidelineSymbolism: string | null = guideline.symbolism ? System.encryptDataWithUserKey(guideline.symbolism, userEncryptionKey) : null; - const encryptedGuidelineMotifs: string | null = guideline.motifs ? System.encryptDataWithUserKey(guideline.motifs, userEncryptionKey) : null; - const encryptedGuidelineNarrativeVoice: string | null = guideline.narrative_voice ? System.encryptDataWithUserKey(guideline.narrative_voice, userEncryptionKey) : null; - const encryptedGuidelinePacing: string | null = guideline.pacing ? System.encryptDataWithUserKey(guideline.pacing, userEncryptionKey) : null; - const encryptedGuidelineIntendedAudience: string | null = guideline.intended_audience ? System.encryptDataWithUserKey(guideline.intended_audience, userEncryptionKey) : null; - const encryptedGuidelineKeyMessages: string | null = guideline.key_messages ? System.encryptDataWithUserKey(guideline.key_messages, userEncryptionKey) : null; - return GuidelineRepo.insertSyncGuideLine(userId, guideline.book_id, encryptedGuidelineTone, encryptedGuidelineAtmosphere, encryptedGuidelineWritingStyle, encryptedGuidelineThemes, encryptedGuidelineSymbolism, encryptedGuidelineMotifs, encryptedGuidelineNarrativeVoice, encryptedGuidelinePacing, encryptedGuidelineIntendedAudience, encryptedGuidelineKeyMessages, guideline.last_update, lang); - }); - if (!guidelinesInserted) return false; - - const issuesInserted: boolean = data.issues.every((issue: BookIssuesTable): boolean => { - const encryptedIssueName: string = System.encryptDataWithUserKey(issue.name, userEncryptionKey); - return IssueRepository.insertSyncIssue(issue.issue_id, userId, issue.book_id, encryptedIssueName, issue.hashed_issue_name, issue.last_update, lang); - }); - if (!issuesInserted) return false; - - 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 | null = spell.description ? System.encryptDataWithUserKey(spell.description, userEncryptionKey) : null; - const encryptedAppearance: string | null = spell.appearance ? System.encryptDataWithUserKey(spell.appearance, userEncryptionKey) : null; - const encryptedTags: string | null = spell.tags ? System.encryptDataWithUserKey(spell.tags, userEncryptionKey) : null; - 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; - } -} diff --git a/electron/database/models/EpubStyle.ts b/electron/database/models/EpubStyle.ts deleted file mode 100755 index 88ac97e..0000000 --- a/electron/database/models/EpubStyle.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Default CSS styles for EPUB export formatting. - * - * These styles are applied to the generated EPUB content to ensure - * consistent typography and layout across different e-readers. - * - * @remarks - * - h1 elements: 24px bold font with 24px text indentation - * - p elements: 30px text indentation, 0.7em vertical margins, justified text - * - * All styles use !important to override e-reader default styles. - */ -export const mainStyle: string = `h1 { - font-size: 24px !important; - font-weight: bold !important; - text-indent: 24px !important; -} -p { - text-indent: 30px !important; - margin-top: 0.7em !important; - margin-bottom: 0.7em !important; - text-align: justify !important; -}` diff --git a/electron/database/models/Export.ts b/electron/database/models/Export.ts deleted file mode 100644 index 50d5df1..0000000 --- a/electron/database/models/Export.ts +++ /dev/null @@ -1,211 +0,0 @@ -import {AlignmentType, Document, HeadingLevel, Packer, Paragraph, TextRun} from "docx"; -import PDFDocument from "pdfkit"; -import JSZip from "jszip"; -import {mainStyle} from "./EpubStyle.js"; -import Chapter, {ChapterContentData, CompleteChapterContent} from "./Chapter.js"; -import {CompleteBookData} from "./Book.js"; -import System from "../System.js"; - -export interface ExportResult { - buffer: Buffer; - fileName: string; -} - -export default class Export { - static async transformToDOCX(bookData: CompleteBookData): Promise { - const bookTitle: string = bookData.title; - const filename: string = `${bookTitle}.docx`; - - const docParagraphs: Paragraph[] = []; - - docParagraphs.push( - new Paragraph({ - children: [ - new TextRun({text: bookTitle, bold: true, size: 48}), - ], - alignment: AlignmentType.CENTER, - spacing: {after: 400}, - }) - ); - - if (bookData.subTitle) { - docParagraphs.push( - new Paragraph({ - children: [ - new TextRun({text: bookData.subTitle, italics: true, size: 32}), - ], - alignment: AlignmentType.CENTER, - spacing: {after: 300}, - }) - ); - } - - if (bookData.summary) { - docParagraphs.push( - new Paragraph({ - children: [ - new TextRun({text: bookData.summary, size: 24, italics: true}), - ], - alignment: AlignmentType.JUSTIFIED, - spacing: {after: 400}, - }) - ); - } - - const chapters: ChapterContentData[] = Chapter.getChaptersOrSheet(bookData.chapters); - - for (const chapter of chapters) { - if (!chapter.content) continue; - - docParagraphs.push( - new Paragraph({ - text: chapter.title, - heading: HeadingLevel.HEADING_1, - pageBreakBefore: true, - alignment: AlignmentType.CENTER, - spacing: {after: 200}, - }) - ); - - const paragraphs: string[] = chapter.content.split(/\r?\n/); - - for (const paragraph of paragraphs) { - if (paragraph.trim() === "") continue; - docParagraphs.push( - new Paragraph({ - children: [new TextRun({text: paragraph, size: 24})], - alignment: AlignmentType.JUSTIFIED, - spacing: {after: 200}, - }) - ); - } - } - - const doc: Document = new Document({ - sections: [{children: docParagraphs}], - }); - - const buffer: Buffer = await Packer.toBuffer(doc) as Buffer; - - return {buffer, fileName: filename}; - } - - static async transformToPDF(bookData: CompleteBookData): Promise { - const bookTitle: string = bookData.title; - const filename: string = `${bookTitle}.pdf`; - const chunks: Buffer[] = []; - const pdfDoc: PDFKit.PDFDocument = new PDFDocument(); - - pdfDoc.on('data', (chunk: Buffer): void => { - chunks.push(chunk); - }); - - pdfDoc.fontSize(20).text(bookTitle, {align: 'center'}); - pdfDoc.moveDown(); - - if (bookData.subTitle && bookData.subTitle.trim() !== '') { - pdfDoc.fontSize(16).text(bookData.subTitle, {align: 'center'}); - pdfDoc.moveDown(); - } - - if (bookData.summary && bookData.summary.trim() !== '') { - pdfDoc.fontSize(12).text(bookData.summary, {align: 'justify'}); - pdfDoc.moveDown(); - } - - const chapters: ChapterContentData[] = Chapter.getChaptersOrSheet(bookData.chapters); - - for (const chapter of chapters) { - if (!chapter.content) continue; - pdfDoc.addPage(); - pdfDoc.fontSize(16).text(chapter.title, {align: 'center'}); - pdfDoc.moveDown(); - pdfDoc.fontSize(12).text(chapter.content, {align: 'justify'}); - } - - pdfDoc.end(); - - await new Promise((resolve: () => void, reject: (reason: Error) => void) => { - pdfDoc.on('end', resolve); - pdfDoc.on('error', reject); - }); - - const pdfBuffer: Buffer = Buffer.concat(chunks); - return {buffer: pdfBuffer, fileName: filename}; - } - - static async transformToEpub(bookData: CompleteBookData): Promise { - const bookTitle: string = bookData.title; - const bookId: string = bookData.bookId; - const epub: JSZip = new JSZip(); - - epub.file('mimetype', 'application/epub+zip', {compression: 'STORE'}); - epub.file('META-INF/container.xml', ` - - - - -`); - - let contentOpf: string = ` - - - ${bookTitle}${bookData.subTitle ? ' - ' + bookData.subTitle : ''} - fr - urn:uuid:${bookId} - ${bookData.userInfos.firstName} ${bookData.userInfos.lastName} - ERitors Scribe - - - `; - - let spine: string = ``; - - const hasRegularChapters: boolean = bookData.chapters.some( - (chapter: CompleteChapterContent): boolean => chapter.order > 0 - ); - const chaptersToExport: CompleteChapterContent[] = hasRegularChapters - ? bookData.chapters.filter((chapter: CompleteChapterContent): boolean => chapter.order > 0) - : bookData.chapters.filter((chapter: CompleteChapterContent): boolean => chapter.order === -1); - - for (const chapter of chaptersToExport) { - if (!chapter.content) continue; - const chapterIndex: string = `chapter${chapter.order}`; - const htmlContent: string = Chapter.tipTapToHtml(JSON.parse(chapter.content) as JSON); - - const xhtmlPage: string = ` - - - ${chapter.title} - - - - ${htmlContent} - - `; - - epub.file(`OEBPS/${chapterIndex}.xhtml`, xhtmlPage); - contentOpf += ``; - spine += ``; - } - - spine += ``; - - contentOpf += ` - `; - contentOpf += spine; - contentOpf += ``; - - epub.file('OEBPS/content.opf', contentOpf); - epub.file('OEBPS/styles.css', mainStyle); - - if (bookData.coverImage) { - const imageBuffer: Buffer = Buffer.from(bookData.coverImage, 'base64'); - epub.file('OEBPS/cover.jpg', imageBuffer); - } - - const epubBuffer: Buffer = await epub.generateAsync({type: 'nodebuffer'}) as Buffer; - - return {buffer: epubBuffer, fileName: `${bookTitle}.epub`}; - } -} diff --git a/electron/database/models/GuideLine.ts b/electron/database/models/GuideLine.ts deleted file mode 100644 index 84eb4a1..0000000 --- a/electron/database/models/GuideLine.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import GuidelineRepo, { GuideLineAIQuery, GuideLineQuery } from "../repositories/guideline.repository.js"; - -export interface SyncedGuideLine { - lastUpdate: number; -} - -export interface SyncedAIGuideLine { - lastUpdate: number; -} - -export interface GuideLineProps { - tone: string; - atmosphere: string; - writingStyle: string; - themes: string; - symbolism: string; - motifs: string; - narrativeVoice: string; - pacing: string; - intendedAudience: string; - keyMessages: string; -} - -export interface GuideLineAI { - narrativeType: number | null; - dialogueType: number | null; - globalResume: string | null; - atmosphere: string | null; - verbeTense: number | null; - langue: number | null; - currentResume: string | null; - themes: string | null; -} - -export default class GuideLine { - /** - * Retrieves and decrypts the guideline 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'), defaults to 'fr' - * @returns The decrypted guideline properties or null if not found - */ - public static async getGuideLine(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): Promise { - const guideLineResults: GuideLineQuery[] = GuidelineRepo.fetchGuideLine(userId, bookId, lang); - if (guideLineResults.length === 0) { - return null; - } - const guideLineData: GuideLineQuery = guideLineResults[0]; - const encryptionKey: string = getUserEncryptionKey(userId); - return { - tone: guideLineData.tone ? System.decryptDataWithUserKey(guideLineData.tone, encryptionKey) : '', - atmosphere: guideLineData.atmosphere ? System.decryptDataWithUserKey(guideLineData.atmosphere, encryptionKey) : '', - writingStyle: guideLineData.writing_style ? System.decryptDataWithUserKey(guideLineData.writing_style, encryptionKey) : '', - themes: guideLineData.themes ? System.decryptDataWithUserKey(guideLineData.themes, encryptionKey) : '', - symbolism: guideLineData.symbolism ? System.decryptDataWithUserKey(guideLineData.symbolism, encryptionKey) : '', - motifs: guideLineData.motifs ? System.decryptDataWithUserKey(guideLineData.motifs, encryptionKey) : '', - narrativeVoice: guideLineData.narrative_voice ? System.decryptDataWithUserKey(guideLineData.narrative_voice, encryptionKey) : '', - pacing: guideLineData.pacing ? System.decryptDataWithUserKey(guideLineData.pacing, encryptionKey) : '', - intendedAudience: guideLineData.intended_audience ? System.decryptDataWithUserKey(guideLineData.intended_audience, encryptionKey) : '', - keyMessages: guideLineData.key_messages ? System.decryptDataWithUserKey(guideLineData.key_messages, encryptionKey) : '', - }; - } - - /** - * Updates or creates a guideline for a specific book with encrypted data. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param tone - The tone setting for the book (nullable) - * @param atmosphere - The atmosphere setting for the book (nullable) - * @param writingStyle - The writing style for the book (nullable) - * @param themes - The themes for the book (nullable) - * @param symbolism - The symbolism elements for the book (nullable) - * @param motifs - The motifs for the book (nullable) - * @param narrativeVoice - The narrative voice for the book (nullable) - * @param pacing - The pacing setting for the book (nullable) - * @param keyMessages - The key messages for the book (nullable) - * @param intendedAudience - The intended audience for the book (nullable) - * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr' - * @returns True if the update was successful, false otherwise - */ - public static async updateGuideLine( - userId: string, - bookId: string, - tone: string | null, - atmosphere: string | null, - writingStyle: string | null, - themes: string | null, - symbolism: string | null, - motifs: string | null, - narrativeVoice: string | null, - pacing: string | null, - keyMessages: string | null, - intendedAudience: string | null, - lang: 'fr' | 'en' = 'fr' - ): Promise { - const encryptionKey: string = getUserEncryptionKey(userId); - const encryptedTone: string = tone ? System.encryptDataWithUserKey(tone, encryptionKey) : ''; - const encryptedAtmosphere: string = atmosphere ? System.encryptDataWithUserKey(atmosphere, encryptionKey) : ''; - const encryptedWritingStyle: string = writingStyle ? System.encryptDataWithUserKey(writingStyle, encryptionKey) : ''; - const encryptedThemes: string = themes ? System.encryptDataWithUserKey(themes, encryptionKey) : ''; - const encryptedSymbolism: string = symbolism ? System.encryptDataWithUserKey(symbolism, encryptionKey) : ''; - const encryptedMotifs: string = motifs ? System.encryptDataWithUserKey(motifs, encryptionKey) : ''; - const encryptedNarrativeVoice: string = narrativeVoice ? System.encryptDataWithUserKey(narrativeVoice, encryptionKey) : ''; - const encryptedPacing: string = pacing ? System.encryptDataWithUserKey(pacing, encryptionKey) : ''; - const encryptedKeyMessages: string = keyMessages ? System.encryptDataWithUserKey(keyMessages, encryptionKey) : ''; - const encryptedIntendedAudience: string = intendedAudience ? System.encryptDataWithUserKey(intendedAudience, encryptionKey) : ''; - - return GuidelineRepo.updateGuideLine( - userId, - bookId, - encryptedTone, - encryptedAtmosphere, - encryptedWritingStyle, - encryptedThemes, - encryptedSymbolism, - encryptedMotifs, - encryptedNarrativeVoice, - encryptedPacing, - encryptedKeyMessages, - encryptedIntendedAudience, - System.timeStampInSeconds(), - lang - ); - } - - /** - * Retrieves and decrypts the AI guideline 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'), defaults to 'fr' - * @returns The decrypted AI guideline data with default values if not found - * @throws Error if an unexpected error occurs during retrieval - */ - static getGuideLineAI(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): GuideLineAI { - const encryptionKey: string = getUserEncryptionKey(userId); - try { - const aiGuideLineData: GuideLineAIQuery = GuidelineRepo.fetchGuideLineAI(userId, bookId, lang); - return { - narrativeType: aiGuideLineData.narrative_type, - dialogueType: aiGuideLineData.dialogue_type, - globalResume: aiGuideLineData.global_resume ? System.decryptDataWithUserKey(aiGuideLineData.global_resume, encryptionKey) : '', - atmosphere: aiGuideLineData.atmosphere ? System.decryptDataWithUserKey(aiGuideLineData.atmosphere, encryptionKey) : '', - verbeTense: aiGuideLineData.verbe_tense, - themes: aiGuideLineData.themes ? System.decryptDataWithUserKey(aiGuideLineData.themes, encryptionKey) : '', - currentResume: aiGuideLineData.current_resume ? System.decryptDataWithUserKey(aiGuideLineData.current_resume, encryptionKey) : '', - langue: aiGuideLineData.langue - }; - } catch (error: unknown) { - if (error instanceof Error && error.message.includes('not found')) { - return { - narrativeType: 0, - dialogueType: 0, - globalResume: '', - atmosphere: '', - verbeTense: 0, - themes: '', - currentResume: '', - langue: 0 - }; - } - if (error instanceof Error) { - throw new Error(error.message); - } else { - const errorMessage: string = lang === 'fr' - ? "Erreur inconnue lors de la recuperation de la ligne directrice de l'IA." - : "Unknown error while fetching AI guideline."; - throw new Error(errorMessage); - } - } - } - - /** - * Creates or updates an AI guideline for a specific book with encrypted data. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param narrativeType - The narrative type identifier - * @param dialogueType - The dialogue type identifier - * @param plotSummary - The plot summary text to be encrypted - * @param toneAtmosphere - The tone and atmosphere description to be encrypted - * @param verbTense - The verb tense identifier - * @param language - The language identifier - * @param themes - The themes description to be encrypted - * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr' - * @returns True if the operation was successful, false otherwise - */ - public static setAIGuideLine( - userId: string, - bookId: string, - narrativeType: number, - dialogueType: number, - plotSummary: string, - toneAtmosphere: string, - verbTense: number, - language: number, - themes: string, - lang: 'fr' | 'en' = 'fr' - ): boolean { - const encryptionKey: string = getUserEncryptionKey(userId); - const encryptedPlotSummary: string = plotSummary ? System.encryptDataWithUserKey(plotSummary, encryptionKey) : ''; - const encryptedToneAtmosphere: string = toneAtmosphere ? System.encryptDataWithUserKey(toneAtmosphere, encryptionKey) : ''; - const encryptedThemes: string = themes ? System.encryptDataWithUserKey(themes, encryptionKey) : ''; - return GuidelineRepo.insertAIGuideLine( - userId, - bookId, - narrativeType, - dialogueType, - encryptedPlotSummary, - encryptedToneAtmosphere, - verbTense, - language, - encryptedThemes, - System.timeStampInSeconds(), - lang - ); - } -} diff --git a/electron/database/models/Incident.ts b/electron/database/models/Incident.ts deleted file mode 100644 index 23ba62c..0000000 --- a/electron/database/models/Incident.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import { ActChapter } from "./Act.js"; -import IncidentRepository, { IncidentQuery } from "../repositories/incident.repository.js"; -import RemovedItem from "./RemovedItem.js"; - -export interface IncidentStory { - incidentTitle: string; - incidentSummary: string; - chapterSummary: string; - chapterGoal: string; -} - -export interface SyncedIncident { - id: string; - name: string; - lastUpdate: number; -} - -export interface IncidentProps { - incidentId: string; - title: string; - summary: string; - chapters?: ActChapter[]; -} - -export default class Incident { - /** - * Creates a new incident for a book. - * Encrypts the incident name and generates a hashed version for indexing. - * @param userId - The unique identifier of the user creating the incident - * @param bookId - The unique identifier of the book to add the incident to - * @param name - The plain text name of the incident - * @param lang - The language for error messages (defaults to 'fr') - * @param existingIncidentId - Optional existing incident ID to use instead of generating a new one - * @returns The unique identifier of the created incident - */ - public static addNewIncident( - userId: string, - bookId: string, - name: string, - lang: 'fr' | 'en' = 'fr', - existingIncidentId?: string - ): string { - const userKey: string = getUserEncryptionKey(userId); - const encryptedName: string = System.encryptDataWithUserKey(name, userKey); - const hashedName: string = System.hashElement(name); - const incidentId: string = existingIncidentId || System.createUniqueId(); - return IncidentRepository.insertNewIncident(incidentId, userId, bookId, encryptedName, hashedName, lang); - } - - /** - * Retrieves all incidents for a specific book with their associated chapters. - * Decrypts incident titles and summaries using the user's encryption key. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param actChapters - Array of chapters from acts to associate with incidents - * @param lang - The language for error messages (defaults to 'fr') - * @returns A promise resolving to an array of incident properties with decrypted data - */ - public static async getIncitentsIncidents( - userId: string, - bookId: string, - actChapters: ActChapter[], - lang: 'fr' | 'en' = 'fr' - ): Promise { - const incidentQueryResults: IncidentQuery[] = IncidentRepository.fetchAllIncitentIncidents(userId, bookId, lang); - const incidents: IncidentProps[] = []; - const userKey: string = getUserEncryptionKey(userId); - - if (incidentQueryResults.length > 0) { - for (const incidentRecord of incidentQueryResults) { - const associatedChapters: ActChapter[] = []; - for (const chapter of actChapters) { - if (chapter.incidentId === incidentRecord.incident_id) { - associatedChapters.push(chapter); - } - } - incidents.push({ - incidentId: incidentRecord.incident_id, - title: incidentRecord.title ? System.decryptDataWithUserKey(incidentRecord.title, userKey) : '', - summary: incidentRecord.summary ? System.decryptDataWithUserKey(incidentRecord.summary, userKey) : '', - chapters: associatedChapters - }); - } - } - return incidents; - } - - /** - * Removes an incident from a book. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param incidentId - The unique identifier of the incident to remove - * @param deletedAt - The timestamp of deletion - * @param lang - The language for error messages (defaults to 'fr') - * @returns True if the incident was successfully deleted, false otherwise - */ - public static removeIncident( - userId: string, - bookId: string, - incidentId: string, - deletedAt: number = System.timeStampInSeconds(), - lang: 'fr' | 'en' = 'fr' - ): boolean { - const deleted: boolean = IncidentRepository.deleteIncident(userId, bookId, incidentId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'book_incidents', incidentId, deletedAt, lang); - } - return deleted; - } -} diff --git a/electron/database/models/Issue.ts b/electron/database/models/Issue.ts deleted file mode 100644 index e8dbc31..0000000 --- a/electron/database/models/Issue.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import IssueRepository, { IssueQuery } from "../repositories/issue.repository.js"; -import RemovedItem from "./RemovedItem.js"; - -/** - * Represents a synced issue with its metadata. - */ -export interface SyncedIssue { - id: string; - name: string; - lastUpdate: number; -} - -/** - * Represents the basic properties of an issue. - */ -export interface IssueProps { - id: string; - name: string; -} - -/** - * Model class for managing issues associated with books. - * Provides methods for CRUD operations on issues with encryption support. - */ -export default class Issue { - /** - * Retrieves all issues associated with a specific book. - * Decrypts issue names using the user's encryption key. - * - * @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'). Defaults to 'fr'. - * @returns A promise resolving to an array of decrypted issues. - */ - public static async getIssuesFromBook( - userId: string, - bookId: string, - lang: 'fr' | 'en' = 'fr' - ): Promise { - const issueQueryResults: IssueQuery[] = IssueRepository.fetchIssuesFromBook(userId, bookId, lang); - const userEncryptionKey: string = getUserEncryptionKey(userId); - const decryptedIssues: IssueProps[] = []; - - if (issueQueryResults.length > 0) { - for (const issueRecord of issueQueryResults) { - decryptedIssues.push({ - id: issueRecord.issue_id, - name: System.decryptDataWithUserKey(issueRecord.name, userEncryptionKey) - }); - } - } - - return decryptedIssues; - } - - /** - * Creates a new issue for a book. - * Encrypts and hashes the issue name before storage. - * - * @param userId - The unique identifier of the user. - * @param bookId - The unique identifier of the book. - * @param name - The plain text name of the issue. - * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @param existingIssueId - Optional existing issue ID for syncing purposes. - * @returns The unique identifier of the created issue. - */ - public static addNewIssue( - userId: string, - bookId: string, - name: string, - lang: 'fr' | 'en' = 'fr', - existingIssueId?: string - ): string { - const userEncryptionKey: string = getUserEncryptionKey(userId); - const encryptedName: string = System.encryptDataWithUserKey(name, userEncryptionKey); - const hashedName: string = System.hashElement(name); - const issueId: string = existingIssueId || System.createUniqueId(); - - return IssueRepository.insertNewIssue(issueId, userId, bookId, encryptedName, hashedName, lang); - } - - /** - * Removes an issue from the database. - * - * @param userId - The unique identifier of the user. - * @param bookId - The unique identifier of the book. - * @param issueId - The unique identifier of the issue to remove. - * @param deletedAt - The timestamp of deletion. - * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @returns True if the issue was successfully removed, false otherwise. - */ - public static removeIssue( - userId: string, - bookId: string, - issueId: string, - deletedAt: number = System.timeStampInSeconds(), - lang: 'fr' | 'en' = 'fr' - ): boolean { - const deleted: boolean = IssueRepository.deleteIssue(userId, issueId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'book_issues', issueId, deletedAt, lang); - } - return deleted; - } -} diff --git a/electron/database/models/Location.ts b/electron/database/models/Location.ts deleted file mode 100644 index 8e000d3..0000000 --- a/electron/database/models/Location.ts +++ /dev/null @@ -1,383 +0,0 @@ -import LocationRepo, { - LocationByTagResult, - LocationElementQueryResult, - LocationQueryResult -} from "../repositories/location.repository.js"; -import System from "../System.js"; -import {getUserEncryptionKey} from "../keyManager.js"; -import BookRepo, {BookToolsTable} from "../repositories/book.repository.js"; -import RemovedItem from "./RemovedItem.js"; - -export interface SubElement { - id: string; - name: string; - description: string; -} - -export interface Element { - id: string; - name: string; - description: string; - subElements: SubElement[]; -} - -export interface LocationProps { - id: string; - name: string; - elements: Element[]; - seriesLocationId?: string | null; -} - -export interface LocationListResponse { - locations: LocationProps[]; - enabled: boolean; -} - -export interface SyncedLocation { - id: string; - name: string; - lastUpdate: number; - elements: SyncedLocationElement[]; -} - -export interface SyncedLocationElement { - id: string; - name: string; - lastUpdate: number; - subElements: SyncedLocationSubElement[]; -} - -export interface SyncedLocationSubElement { - id: string; - name: string; - lastUpdate: number; -} - -export default class Location { - /** - * Retrieves all locations for a given user and book. - * @param userId - The user's unique identifier. - * @param bookId - The book's unique identifier. - * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @returns LocationListResponse containing an array of locations and enabled flag. - */ - static getAllLocations(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LocationListResponse { - const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang); - const enabled: boolean = bookTools ? bookTools.locations_enabled === 1 : false; - - const locationRecords: LocationQueryResult[] = LocationRepo.getLocation(userId, bookId, lang); - if (!locationRecords || locationRecords.length === 0) { - return { locations: [], enabled }; - } - const userKey: string = getUserEncryptionKey(userId); - - const locationArray: LocationProps[] = []; - - for (const record of locationRecords) { - let location = locationArray.find(loc => loc.id === record.loc_id); - - if (!location) { - const decryptedName: string = System.decryptDataWithUserKey(record.loc_name, userKey); - location = { - id: record.loc_id, - name: decryptedName, - elements: [], - seriesLocationId: record.series_location_id || null, - }; - locationArray.push(location); - } - - if (record.element_id) { - let element = location.elements.find(elem => elem.id === record.element_id); - if (!element) { - const decryptedName: string = System.decryptDataWithUserKey(record.element_name, userKey); - const decryptedDescription: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : ''; - - element = { - id: record.element_id, - name: decryptedName, - description: decryptedDescription, - subElements: [] - }; - location.elements.push(element); - } - - if (record.sub_element_id) { - const subElementExists = element.subElements.some(sub => sub.id === record.sub_element_id); - - if (!subElementExists) { - const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, userKey); - const decryptedDescription: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : ''; - - element.subElements.push({ - id: record.sub_element_id, - name: decryptedName, - description: decryptedDescription - }); - } - } - } - } - return { locations: locationArray, enabled }; - } - - /** - * Adds a new location section for a book. - * @param userId - The user's unique identifier. - * @param locationName - The name of the location to create. - * @param bookId - The book's unique identifier. - * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @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, 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, seriesLocationId); - } - - /** - * Adds a new element to a location. - * @param userId - The user's unique identifier. - * @param locationId - The parent location's unique identifier. - * @param elementName - The name of the element to create. - * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @param existingElementId - Optional existing element ID to use instead of generating a new one. - * @returns The result of the insert operation. - */ - static addLocationElement(userId: string, locationId: string, elementName: string, lang: 'fr' | 'en' = 'fr', existingElementId?: string): string { - const userKey: string = getUserEncryptionKey(userId); - const hashedName: string = System.hashElement(elementName); - const encryptedName: string = System.encryptDataWithUserKey(elementName, userKey); - const elementId: string = existingElementId || System.createUniqueId(); - return LocationRepo.insertLocationElement(userId, elementId, locationId, encryptedName, hashedName, lang) - } - - /** - * Adds a new sub-element to a location element. - * @param userId - The user's unique identifier. - * @param elementId - The parent element's unique identifier. - * @param subElementName - The name of the sub-element to create. - * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @param existingSubElementId - Optional existing sub-element ID to use instead of generating a new one. - * @returns The result of the insert operation. - */ - static addLocationSubElement(userId: string, elementId: string, subElementName: string, lang: 'fr' | 'en' = 'fr', existingSubElementId?: string): string { - const userKey: string = getUserEncryptionKey(userId); - const hashedName: string = System.hashElement(subElementName); - const encryptedName: string = System.encryptDataWithUserKey(subElementName, userKey); - const subElementId: string = existingSubElementId || System.createUniqueId(); - return LocationRepo.insertLocationSubElement(userId, subElementId, elementId, encryptedName, hashedName, lang) - } - - /** - * Updates multiple location sections along with their elements and sub-elements. - * @param userId - The user's unique identifier. - * @param locations - Array of location properties to update. - * @param lang - The language for response messages ('fr' or 'en'). Defaults to 'fr'. - * @returns An object indicating success and a localized message. - */ - static updateLocationSection(userId: string, locations: LocationProps[], lang: 'fr' | 'en' = 'fr'): { valid: boolean; message: string } { - const userKey: string = getUserEncryptionKey(userId); - - for (const location of locations) { - const hashedLocationName: string = System.hashElement(location.name); - const encryptedLocationName: string = System.encryptDataWithUserKey(location.name, userKey); - LocationRepo.updateLocationSection(userId, location.id, encryptedLocationName, hashedLocationName, System.timeStampInSeconds(), lang) - for (const element of location.elements) { - const hashedElementName: string = System.hashElement(element.name); - const encryptedElementName: string = System.encryptDataWithUserKey(element.name, userKey); - const encryptedElementDescription: string = element.description ? System.encryptDataWithUserKey(element.description, userKey) : ''; - LocationRepo.updateLocationElement(userId, element.id, encryptedElementName, hashedElementName, encryptedElementDescription, System.timeStampInSeconds(), lang) - for (const subElement of element.subElements) { - const hashedSubElementName: string = System.hashElement(subElement.name); - const encryptedSubElementName: string = System.encryptDataWithUserKey(subElement.name, userKey); - const encryptedSubElementDescription: string = subElement.description ? System.encryptDataWithUserKey(subElement.description, userKey) : ''; - LocationRepo.updateLocationSubElement(userId, subElement.id, encryptedSubElementName, hashedSubElementName, encryptedSubElementDescription, System.timeStampInSeconds(), lang) - } - } - } - return { - valid: true, - message: lang === 'fr' ? 'Les sections ont été mis à jour.' : 'Sections have been updated.' - } - } - - /** - * 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. - * @param bookId - The book's unique identifier. - * @param locationId - The location's unique identifier to delete. - * @param deletedAt - The timestamp of deletion. - * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @returns The result of the delete operation. - */ - static deleteLocationSection(userId: string, bookId: string, locationId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = LocationRepo.deleteLocationSection(userId, locationId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'book_location', locationId, deletedAt, lang); - } - return deleted; - } - - /** - * Deletes a location element and all its associated sub-elements. - * @param userId - The user's unique identifier. - * @param bookId - The book's unique identifier. - * @param elementId - The element's unique identifier to delete. - * @param deletedAt - The timestamp of deletion. - * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @returns The result of the delete operation. - */ - static deleteLocationElement(userId: string, bookId: string, elementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = LocationRepo.deleteLocationElement(userId, elementId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'location_element', elementId, deletedAt, lang); - } - return deleted; - } - - /** - * Deletes a location sub-element. - * @param userId - The user's unique identifier. - * @param bookId - The book's unique identifier. - * @param subElementId - The sub-element's unique identifier to delete. - * @param deletedAt - The timestamp of deletion. - * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @returns The result of the delete operation. - */ - static deleteLocationSubElement(userId: string, bookId: string, subElementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = LocationRepo.deleteLocationSubElement(userId, subElementId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'location_sub_element', subElementId, deletedAt, lang); - } - return deleted; - } - - /** - * Retrieves location tags (elements or sub-elements) for tagging purposes. - * Returns sub-elements when an element has multiple sub-elements, otherwise returns the element itself. - * @param userId - The user's unique identifier. - * @param bookId - The book's unique identifier. - * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @returns An array of sub-elements suitable for tagging. - */ - static getLocationTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SubElement[] { - const tagRecords: LocationElementQueryResult[] = LocationRepo.fetchLocationTags(userId, bookId, lang); - if (!tagRecords || tagRecords.length === 0) return []; - const userKey: string = getUserEncryptionKey(userId); - - const elementCounts = new Map(); - tagRecords.forEach((record: LocationElementQueryResult): void => { - elementCounts.set(record.element_id, (elementCounts.get(record.element_id) || 0) + 1); - }); - - const subElements: SubElement[] = []; - const processedIds = new Set(); - - for (const record of tagRecords) { - const elementCount: number = elementCounts.get(record.element_id) || 0; - - if (elementCount > 1 && record.sub_element_id) { - if (processedIds.has(record.sub_element_id)) continue; - - subElements.push({ - id: record.sub_element_id, - name: System.decryptDataWithUserKey(record.sub_elem_name, userKey), - description: record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : '' - }); - processedIds.add(record.sub_element_id); - } else if (elementCount === 1 && !record.sub_element_id) { - if (processedIds.has(record.element_id)) continue; - - subElements.push({ - id: record.element_id, - name: System.decryptDataWithUserKey(record.element_name, userKey), - description: record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : '' - }); - processedIds.add(record.element_id); - } - } - return subElements; - } - - /** - * Retrieves location elements filtered by specific tag IDs. - * @param userId - The user's unique identifier. - * @param locations - Array of location tag IDs to filter by. - * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @returns An array of elements with their associated sub-elements. - */ - static getLocationsByTags(userId: string, locations: string[], lang: 'fr' | 'en' = 'fr'): Element[] { - const locationTagRecords: LocationByTagResult[] = LocationRepo.fetchLocationsByTags(userId, locations, lang); - if (!locationTagRecords || locationTagRecords.length === 0) return []; - const userKey: string = getUserEncryptionKey(userId); - const locationElements: Element[] = []; - for (const record of locationTagRecords) { - let element: Element | undefined = locationElements.find((elem: Element): boolean => elem.name === record.element_name); - if (!element) { - const decryptedName: string = System.decryptDataWithUserKey(record.element_name, userKey); - const decryptedDescription: string = record.element_description ? System.decryptDataWithUserKey(record.element_description, userKey) : ''; - element = { - id: '', - name: decryptedName, - description: decryptedDescription, - subElements: [] - }; - locationElements.push(element); - } - if (record.sub_elem_name) { - const subElementExists: boolean = element.subElements.some(sub => sub.name === record.sub_elem_name); - if (!subElementExists) { - const decryptedName: string = System.decryptDataWithUserKey(record.sub_elem_name, userKey); - const decryptedDescription: string = record.sub_elem_description ? System.decryptDataWithUserKey(record.sub_elem_description, userKey) : ''; - element.subElements.push({ - id: '', - name: decryptedName, - description: decryptedDescription - }); - } - } - } - return locationElements; - } - - /** - * Generates a formatted description string from an array of location elements. - * @param locations - Array of location elements to describe. - * @returns A formatted string with location names and descriptions. - */ - static locationsDescription(locations: Element[]): string { - return locations.map((location: Element): string => { - const descriptionFields: string[] = []; - if (location.name) descriptionFields.push(`Nom : ${location.name}`); - if (location.description) descriptionFields.push(`Description : ${location.description}`); - return descriptionFields.join('\n'); - }).join('\n\n'); - } - -} diff --git a/electron/database/models/Model.ts b/electron/database/models/Model.ts deleted file mode 100644 index 0be9745..0000000 --- a/electron/database/models/Model.ts +++ /dev/null @@ -1,278 +0,0 @@ -/** - * Supported OpenAI GPT model identifiers. - */ -export type GPTModel = "gpt-4o-mini" | "gpt-4o-turbo" | "gpt-3.5-turbo" | "gpt-4o" | "gpt-4.1" | "gpt-4.1-nano"; - -/** - * Supported Anthropic Claude model identifiers. - */ -export type AnthropicModel = - "claude-3-7-sonnet-20250219" - | "claude-sonnet-4-20250514" - | "claude-sonnet-4-5-20250929" - | "claude-3-5-haiku-20241022" - | "claude-3-5-sonnet-20241022" - | "claude-3-5-sonnet-20240620" - | "claude-3-opus-20240229"; - -/** - * Supported Google Gemini model identifiers. - */ -export type GeminiModel = - | "gemini-2.0-flash-001" - | "gemini-2.0-flash-lite-001" - | "gemini-2.5-flash" - | "gemini-2.5-flash-lite" - | "gemini-2.5-pro"; - -/** - * Configuration object representing an AI model with its pricing information. - */ -export interface AIModelConfig { - /** Unique identifier for the AI model */ - model_id: string; - /** Human-readable display name for the model */ - model_name: string; - /** Brand or provider of the model (e.g., Anthropic, OpenAI, Google) */ - brand: string; - /** Price per input tokens in USD */ - price_token_in: number; - /** Number of input tokens per price unit */ - per_quantity_in: number; - /** Price per output tokens in USD */ - price_token_out: number; - /** Number of output tokens per price unit */ - per_quantity_out: number; -} - -/** - * Array of all available AI models with their configurations and pricing. - * Includes models from Anthropic (Claude), Google (Gemini), and OpenAI (GPT). - */ -export const AIModels: AIModelConfig[] = [ - { - "model_id": "claude-3-5-haiku-20241022", - "model_name": "Claude Haiku 3.5", - "brand": "Anthropic", - "price_token_in": 0.8, - "per_quantity_in": 1000000, - "price_token_out": 4, - "per_quantity_out": 1000000 - }, - { - "model_id": "claude-3-5-sonnet-20241022", - "model_name": "Claude Sonnet 3.5", - "brand": "Anthropic", - "price_token_in": 3, - "per_quantity_in": 1000000, - "price_token_out": 15, - "per_quantity_out": 1000000 - }, - { - "model_id": "claude-3-7-sonnet-20250219", - "model_name": "Claude Sonnet 3.7", - "brand": "Anthropic", - "price_token_in": 3, - "per_quantity_in": 1000000, - "price_token_out": 15, - "per_quantity_out": 1000000 - }, - { - "model_id": "claude-3-haiku-20240307", - "model_name": "Claude Haiku 3", - "brand": "Anthropic", - "price_token_in": 0.25, - "per_quantity_in": 1000000, - "price_token_out": 1.25, - "per_quantity_out": 1000000 - }, - { - "model_id": "claude-3-opus-20240229", - "model_name": "Claude Opus 3", - "brand": "Anthropic", - "price_token_in": 15, - "per_quantity_in": 1000000, - "price_token_out": 75, - "per_quantity_out": 1000000 - }, - { - "model_id": "claude-opus-4-20250514", - "model_name": "Claude Opus 4", - "brand": "Anthropic", - "price_token_in": 15, - "per_quantity_in": 1000000, - "price_token_out": 75, - "per_quantity_out": 1000000 - }, - { - "model_id": "claude-sonnet-4-20250514", - "model_name": "Claude Sonnet 4", - "brand": "Anthropic", - "price_token_in": 3, - "per_quantity_in": 1000000, - "price_token_out": 15, - "per_quantity_out": 1000000 - }, - { - "model_id": "claude-sonnet-4-5-20250929", - "model_name": "Claude Sonnet 4.5", - "brand": "Anthropic", - "price_token_in": 3, - "per_quantity_in": 1000000, - "price_token_out": 15, - "per_quantity_out": 1000000 - }, - { - "model_id": "gemini-2.0-flash-001", - "model_name": "Gemini 2.0 Flash", - "brand": "Google", - "price_token_in": 0.1, - "per_quantity_in": 1000000, - "price_token_out": 0.4, - "per_quantity_out": 1000000 - }, - { - "model_id": "gemini-2.0-flash-lite-001", - "model_name": "Gemini 2.0 Flash-Lite", - "brand": "Google", - "price_token_in": 0.075, - "per_quantity_in": 1000000, - "price_token_out": 0.3, - "per_quantity_out": 1000000 - }, - { - "model_id": "gemini-2.5-flash", - "model_name": "Gemini 2.5 Flash", - "brand": "Google", - "price_token_in": 0.3, - "per_quantity_in": 1000000, - "price_token_out": 2.5, - "per_quantity_out": 1000000 - }, - { - "model_id": "gemini-2.5-flash-lite", - "model_name": "Gemini 2.5 Flash-Lite", - "brand": "Google", - "price_token_in": 0.1, - "per_quantity_in": 1000000, - "price_token_out": 0.4, - "per_quantity_out": 1000000 - }, - { - "model_id": "gemini-2.5-pro", - "model_name": "Gemini 2.5 Pro", - "brand": "Google", - "price_token_in": 1.25, - "per_quantity_in": 1000000, - "price_token_out": 10, - "per_quantity_out": 1000000 - }, - { - "model_id": "gpt-3.5-turbo", - "model_name": "GPT-3.5 Turbo", - "brand": "OpenAI", - "price_token_in": 0.5, - "per_quantity_in": 1000000, - "price_token_out": 1.5, - "per_quantity_out": 1000000 - }, - { - "model_id": "gpt-4", - "model_name": "GPT-4", - "brand": "OpenAI", - "price_token_in": 30, - "per_quantity_in": 1000000, - "price_token_out": 60, - "per_quantity_out": 1000000 - }, - { - "model_id": "gpt-4-turbo", - "model_name": "GPT-4 Turbo", - "brand": "OpenAI", - "price_token_in": 10, - "per_quantity_in": 1000000, - "price_token_out": 30, - "per_quantity_out": 1000000 - }, - { - "model_id": "gpt-4.1", - "model_name": "GPT-4.1", - "brand": "OpenAI", - "price_token_in": 2, - "per_quantity_in": 1000000, - "price_token_out": 8, - "per_quantity_out": 1000000 - }, - { - "model_id": "gpt-4.1-mini", - "model_name": "GPT-4.1 Mini", - "brand": "OpenAI", - "price_token_in": 0.4, - "per_quantity_in": 1000000, - "price_token_out": 0.6, - "per_quantity_out": 1000000 - }, - { - "model_id": "gpt-4.1-nano", - "model_name": "GPT-4.1 Nano", - "brand": "OpenAI", - "price_token_in": 0.1, - "per_quantity_in": 1000000, - "price_token_out": 0.4, - "per_quantity_out": 1000000 - }, - { - "model_id": "gpt-4o", - "model_name": "GPT-4o", - "brand": "OpenAI", - "price_token_in": 5, - "per_quantity_in": 1000000, - "price_token_out": 20, - "per_quantity_out": 1000000 - }, - { - "model_id": "gpt-4o-2024-11-20", - "model_name": "GPT-4o (2024-11-20)", - "brand": "OpenAI", - "price_token_in": 5, - "per_quantity_in": 1000000, - "price_token_out": 15, - "per_quantity_out": 1000000 - }, - { - "model_id": "gpt-4o-mini", - "model_name": "GPT-4o Mini", - "brand": "OpenAI", - "price_token_in": 0.6, - "per_quantity_in": 1000000, - "price_token_out": 2.4, - "per_quantity_out": 1000000 - }, - { - "model_id": "gpt-5", - "model_name": "GPT 5", - "brand": "OpenAI", - "price_token_in": 1.25, - "per_quantity_in": 1000000, - "price_token_out": 10, - "per_quantity_out": 1000000 - }, - { - "model_id": "gpt-5-mini", - "model_name": "GPT 5 Mini", - "brand": "OpenAI", - "price_token_in": 0.25, - "per_quantity_in": 1000000, - "price_token_out": 2, - "per_quantity_out": 1000000 - }, - { - "model_id": "gpt-5-nano", - "model_name": "GPT 5 Nano", - "brand": "OpenAI", - "price_token_in": 0.05, - "per_quantity_in": 1000000, - "price_token_out": 0.4, - "per_quantity_out": 1000000 - } -] diff --git a/electron/database/models/PlotPoint.ts b/electron/database/models/PlotPoint.ts deleted file mode 100644 index b8876c0..0000000 --- a/electron/database/models/PlotPoint.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import { ActChapter } from "./Act.js"; -import PlotPointRepository, { PlotPointQuery } from "../repositories/plotpoint.repository.js"; -import RemovedItem from "./RemovedItem.js"; - -export interface PlotPointStory { - plotTitle: string; - plotSummary: string; - chapterSummary: string; - chapterGoal: string; -} - -export interface PlotPointProps { - plotPointId: string, - title: string, - summary: string, - linkedIncidentId: string | null, - chapters?: ActChapter[] -} - -export interface SyncedPlotPoint { - id: string; - name: string; - lastUpdate: number; -} - -export default class PlotPoint { - /** - * Retrieves all plot points for a specific book with their associated chapters. - * Decrypts plot point titles and summaries using the user's encryption key. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param actChapters - Array of act chapters to associate with plot points - * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr' - * @returns A promise resolving to an array of plot point properties with their associated chapters - */ - public static async getPlotPoints( - userId: string, - bookId: string, - actChapters: ActChapter[], - lang: 'fr' | 'en' = 'fr' - ): Promise { - const plotPointQueryResults: PlotPointQuery[] = PlotPointRepository.fetchAllPlotPoints(userId, bookId, lang); - const userEncryptionKey: string = getUserEncryptionKey(userId); - const plotPoints: PlotPointProps[] = []; - - if (plotPointQueryResults.length > 0) { - for (const plotPointRow of plotPointQueryResults) { - const associatedChapters: ActChapter[] = []; - - for (const chapter of actChapters) { - if (chapter.plotPointId === plotPointRow.plot_point_id) { - associatedChapters.push(chapter); - } - } - - plotPoints.push({ - plotPointId: plotPointRow.plot_point_id, - title: plotPointRow.title ? System.decryptDataWithUserKey(plotPointRow.title, userEncryptionKey) : '', - summary: plotPointRow.summary ? System.decryptDataWithUserKey(plotPointRow.summary, userEncryptionKey) : '', - linkedIncidentId: plotPointRow.linked_incident_id, - chapters: associatedChapters - }); - } - } - - return plotPoints; - } - - /** - * Creates a new plot point for a book, encrypting the name before storage. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param incidentId - The identifier of the linked incident - * @param name - The name/title of the plot point - * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr' - * @param existingPlotPointId - Optional existing plot point ID to use instead of generating a new one - * @returns The unique identifier of the created plot point - */ - static addNewPlotPoint( - userId: string, - bookId: string, - incidentId: string, - name: string, - lang: 'fr' | 'en' = 'fr', - existingPlotPointId?: string - ): string { - const userEncryptionKey: string = getUserEncryptionKey(userId); - const encryptedName: string = System.encryptDataWithUserKey(name, userEncryptionKey); - const hashedName: string = System.hashElement(name); - const plotPointId: string = existingPlotPointId || System.createUniqueId(); - - return PlotPointRepository.insertNewPlotPoint(plotPointId, userId, bookId, encryptedName, hashedName, incidentId, lang); - } - - /** - * Removes a plot point from the database. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param plotId - The unique identifier of the plot point to remove - * @param deletedAt - The timestamp of deletion - * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr' - * @returns True if the plot point was successfully deleted, false otherwise - */ - static removePlotPoint(userId: string, bookId: string, plotId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = PlotPointRepository.deletePlotPoint(userId, plotId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'book_plot_points', plotId, deletedAt, lang); - } - return deleted; - } -} diff --git a/electron/database/models/RemovedItem.ts b/electron/database/models/RemovedItem.ts deleted file mode 100644 index 0048f31..0000000 --- a/electron/database/models/RemovedItem.ts +++ /dev/null @@ -1,41 +0,0 @@ -import System from '../System.js'; -import RemovedItemsRepository from '../repositories/removed-items.repository.js'; - -/** - * Model class for tracking deleted items for sync purposes. - * Provides the main entry point for recording deletions. - */ -export default class RemovedItem { - /** - * Records a deleted item for sync tracking. - * Must be called BEFORE the actual deletion from the source table. - * - * @param userId - The unique identifier of the user. - * @param bookId - The book ID (null for series items). - * @param tableName - The name of the table from which the item is deleted. - * @param entityId - The UUID of the deleted entity. - * @param deletedAt - The timestamp of deletion (from UI via System.timeStampInSeconds()). - * @param lang - The language for error messages ('fr' or 'en'). Defaults to 'fr'. - * @returns True if the record was inserted successfully. - */ - public static deleteTracker( - userId: string, - bookId: string | null, - tableName: string, - entityId: string, - deletedAt: number, - lang: 'fr' | 'en' = 'fr' - ): boolean { - const removalId: string = System.createUniqueId(); - - return RemovedItemsRepository.insert( - removalId, - tableName, - entityId, - bookId, - userId, - deletedAt, - lang - ); - } -} diff --git a/electron/database/models/Series.ts b/electron/database/models/Series.ts deleted file mode 100644 index 831e6a4..0000000 --- a/electron/database/models/Series.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import SeriesRepo, { SeriesBookResult, SeriesListItem, SeriesResult } from "../repositories/series.repo.js"; -import RemovedItem from "./RemovedItem.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 deletedAt - The timestamp of deletion - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the deletion was successful - */ - public static deleteSeries(userId: string, seriesId: string, deletedAt: number = System.timeStampInSeconds(), 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 deleted: boolean = SeriesRepo.deleteSeries(userId, seriesId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, null, 'book_series', seriesId, deletedAt, lang); - } - return deleted; - } - - /** - * 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 deletedAt - The timestamp of deletion - * @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, deletedAt: number = System.timeStampInSeconds(), 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 deleted: boolean = SeriesRepo.removeBookFromSeries(seriesId, bookId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, null, 'series_books', `${seriesId}_${bookId}`, deletedAt, lang); - } - return deleted; - } - - /** - * 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 - })); - } -} diff --git a/electron/database/models/SeriesCharacter.ts b/electron/database/models/SeriesCharacter.ts deleted file mode 100644 index 41a23da..0000000 --- a/electron/database/models/SeriesCharacter.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import SeriesCharacterRepo, { SeriesCharacterAttributeResult, SeriesCharacterResult } from "../repositories/series-character.repo.js"; -import RemovedItem from "./RemovedItem.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 deletedAt - The timestamp of deletion - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the deletion was successful - */ - public static deleteCharacter(userId: string, characterId: string, deletedAt: number = System.timeStampInSeconds(), 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.'); - } - const deleted: boolean = SeriesCharacterRepo.deleteCharacter(userId, characterId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, null, 'series_characters', characterId, deletedAt, lang); - } - return deleted; - } - - /** - * 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 deletedAt - The timestamp of deletion - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the deletion was successful - */ - public static deleteAttribute(userId: string, attributeId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = SeriesCharacterRepo.deleteAttribute(userId, attributeId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, null, 'series_characters_attributes', attributeId, deletedAt, lang); - } - return deleted; - } - - /** - * 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 }; - } -} diff --git a/electron/database/models/SeriesLocation.ts b/electron/database/models/SeriesLocation.ts deleted file mode 100644 index 6cfa437..0000000 --- a/electron/database/models/SeriesLocation.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import SeriesLocationRepo, { SeriesLocationResult, SeriesLocationElementResult, SeriesLocationSubElementResult } from "../repositories/series-location.repo.js"; -import RemovedItem from "./RemovedItem.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 deletedAt - The timestamp of deletion - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if successful - */ - public static deleteLocation(userId: string, locationId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = SeriesLocationRepo.deleteLocation(userId, locationId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, null, 'series_locations', locationId, deletedAt, lang); - } - return deleted; - } - - /** - * Deletes an element. - * @param userId - The unique identifier of the user - * @param elementId - The unique identifier of the element - * @param deletedAt - The timestamp of deletion - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if successful - */ - public static deleteElement(userId: string, elementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = SeriesLocationRepo.deleteElement(userId, elementId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, null, 'series_location_elements', elementId, deletedAt, lang); - } - return deleted; - } - - /** - * Deletes a sub-element. - * @param userId - The unique identifier of the user - * @param subElementId - The unique identifier of the sub-element - * @param deletedAt - The timestamp of deletion - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if successful - */ - public static deleteSubElement(userId: string, subElementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = SeriesLocationRepo.deleteSubElement(userId, subElementId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, null, 'series_location_sub_elements', subElementId, deletedAt, lang); - } - return deleted; - } -} diff --git a/electron/database/models/SeriesSpell.ts b/electron/database/models/SeriesSpell.ts deleted file mode 100644 index e3dbec9..0000000 --- a/electron/database/models/SeriesSpell.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import SeriesSpellRepo, { SeriesSpellResult, SeriesSpellTagResult } from "../repositories/series-spell.repo.js"; -import RemovedItem from "./RemovedItem.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 deletedAt - The timestamp of deletion - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if successful - */ - public static deleteSpell(userId: string, spellId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = SeriesSpellRepo.deleteSpell(userId, spellId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, null, 'series_spells', spellId, deletedAt, lang); - } - return deleted; - } - - /** - * 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 deletedAt - The timestamp of deletion - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if successful - */ - public static deleteTag(userId: string, tagId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = SeriesSpellRepo.deleteTag(userId, tagId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, null, 'series_spell_tags', tagId, deletedAt, lang); - } - return deleted; - } -} diff --git a/electron/database/models/SeriesSync.ts b/electron/database/models/SeriesSync.ts deleted file mode 100644 index c5da45c..0000000 --- a/electron/database/models/SeriesSync.ts +++ /dev/null @@ -1,1023 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import SeriesSyncRepo, { SyncElementType } from "../repositories/series-sync.repo.js"; -import Sync from "./Sync.js"; -import { - CompleteSeries, - SyncedSeries, - SeriesTable, - SeriesBooksTable, - SeriesCharactersTable, - SeriesCharacterAttributesTable, - SeriesWorldsTable, - SeriesWorldElementsTable, - SeriesLocationsTable, - SeriesLocationElementsTable, - SeriesLocationSubElementsTable, - SeriesSpellsTable, - SeriesSpellTagsTable -} from "./Book.js"; -import SeriesRepo from "../repositories/series.repo.js"; -import BookRepo from "../repositories/book.repository.js"; -import SeriesCharacterRepo from "../repositories/series-character.repo.js"; -import SeriesWorldRepo from "../repositories/series-world.repo.js"; -import SeriesLocationRepo from "../repositories/series-location.repo.js"; -import SeriesSpellRepo from "../repositories/series-spell.repo.js"; - -export interface SeriesSyncUploadPayload { - type: SyncElementType; - bookElementId: string; - field: string; - value: string; -} - -export interface SeriesSyncResult { - success: boolean; - updatedCount: number; -} - -export type { CompleteSeries, SyncedSeries }; - -/** - * Handles series synchronization operations. - * Manages field propagation from book elements to series elements, - * and provides methods for complete series upload/download synchronization. - */ -export default class SeriesSync { - /** - * Uploads a field value from a book element to its linked series element, - * then propagates the change to all other book elements linked to the same series element. - * @param userId - The unique identifier of the user - * @param payload - Contains type, bookElementId, field, and value - * @param lang - The language for error messages ('fr' or 'en') - * @returns Result containing success status and count of updated book elements - */ - static uploadFieldToSeries(userId: string, payload: SeriesSyncUploadPayload, lang: 'fr' | 'en'): SeriesSyncResult { - const { type, bookElementId, field, value } = payload; - - // 1. Get the series element ID linked to the book element - const seriesElementId: string | null = this.getSeriesLink(userId, type, bookElementId, lang); - if (!seriesElementId) { - return { success: false, updatedCount: 0 }; - } - - // 2. Encrypt the value - const userEncryptionKey: string = getUserEncryptionKey(userId); - const encryptedValue: string = System.encryptDataWithUserKey(value, userEncryptionKey); - - // 3. Map the frontend field name to the database column name - const dbColumn: string = this.mapFieldToDbColumn(type, field); - - // 4. Update the series element - const seriesUpdated: boolean = this.updateSeriesElement(userId, type, seriesElementId, dbColumn, encryptedValue, lang); - if (!seriesUpdated) { - return { success: false, updatedCount: 0 }; - } - - // 5. Map the series field to the book field (may be different for some types) - const bookField: string = this.mapSeriesFieldToBookField(type, dbColumn); - - // 6. Update all linked book elements - const updatedCount: number = this.updateLinkedBookElements(userId, type, seriesElementId, bookField, encryptedValue, lang); - - return { success: true, updatedCount }; - } - - /** - * Gets the series element ID linked to a book element. - */ - private static getSeriesLink(userId: string, type: SyncElementType, bookElementId: string, lang: 'fr' | 'en'): string | null { - switch (type) { - case 'character': - return SeriesSyncRepo.getCharacterSeriesLink(userId, bookElementId, lang); - case 'world': - return SeriesSyncRepo.getWorldSeriesLink(userId, bookElementId, lang); - case 'location': - return SeriesSyncRepo.getLocationSeriesLink(userId, bookElementId, lang); - case 'spell': - return SeriesSyncRepo.getSpellSeriesLink(userId, bookElementId, lang); - default: - return null; - } - } - - /** - * Maps frontend field names to database column names. - */ - private static mapFieldToDbColumn(type: SyncElementType, field: string): string { - // Most fields have the same name, but some need mapping - const fieldMappings: Record> = { - character: { - firstName: 'first_name', - lastName: 'last_name', - speechPattern: 'speech_pattern' - }, - world: {}, - location: { - name: 'name', - locName: 'loc_name' - }, - spell: { - powerLevel: 'power_level' - } - }; - - const typeMapping = fieldMappings[type] || {}; - return typeMapping[field] || field; - } - - /** - * Updates a field in the series element. - */ - private static updateSeriesElement( - userId: string, - type: SyncElementType, - seriesElementId: string, - field: string, - encryptedValue: string, - lang: 'fr' | 'en' - ): boolean { - switch (type) { - case 'character': - return SeriesSyncRepo.updateSeriesCharacterField(userId, seriesElementId, field, encryptedValue, lang); - case 'world': - return SeriesSyncRepo.updateSeriesWorldField(userId, seriesElementId, field, encryptedValue, lang); - case 'location': - return SeriesSyncRepo.updateSeriesLocationField(userId, seriesElementId, field, encryptedValue, lang); - case 'spell': - return SeriesSyncRepo.updateSeriesSpellField(userId, seriesElementId, field, encryptedValue, lang); - default: - return false; - } - } - - /** - * Updates all book elements linked to a series element. - */ - private static updateLinkedBookElements( - userId: string, - type: SyncElementType, - seriesElementId: string, - field: string, - encryptedValue: string, - lang: 'fr' | 'en' - ): number { - switch (type) { - case 'character': - return SeriesSyncRepo.updateLinkedBookCharactersField(userId, seriesElementId, field, encryptedValue, lang); - case 'world': - return SeriesSyncRepo.updateLinkedBookWorldsField(userId, seriesElementId, field, encryptedValue, lang); - case 'location': - return SeriesSyncRepo.updateLinkedBookLocationsField(userId, seriesElementId, field, encryptedValue, lang); - case 'spell': - return SeriesSyncRepo.updateLinkedBookSpellsField(userId, seriesElementId, field, encryptedValue, lang); - default: - return 0; - } - } - - /** - * Maps series field names to book field names (they may differ). - */ - private static mapSeriesFieldToBookField(type: SyncElementType, seriesField: string): string { - const fieldMappings: Record> = { - location: { - name: 'loc_name' - } - }; - - const typeMapping = fieldMappings[type] || {}; - return typeMapping[seriesField] || seriesField; - } - - // ===== SYNC METHODS ===== - - /** - * Gets all synced series for a user. - * Delegates to Sync.getSyncedSeries which already implements this functionality. - */ - static getSyncedSeries(userId: string, lang: 'fr' | 'en'): SyncedSeries[] { - return Sync.getSyncedSeries(userId, lang); - } - - /** - * Gets a complete series with all data decrypted for upload to server. - * @param userId - The unique identifier of the user - * @param seriesId - The unique identifier of the series - * @param lang - The language for error messages ('fr' or 'en') - * @returns Complete series object with all decrypted data - */ - static getCompleteSeriesForUpload(userId: string, seriesId: string, lang: 'fr' | 'en'): CompleteSeries { - const userEncryptionKey: string = getUserEncryptionKey(userId); - - // Fetch all series data - use table fetch methods that return arrays - const seriesData = SeriesRepo.fetchSeriesTableForSync(userId, seriesId, lang); - const seriesBooksData = SeriesRepo.fetchSeriesBooksTable(seriesId, lang); - const charactersData = SeriesCharacterRepo.fetchSeriesCharactersTable(userId, seriesId, lang); - const characterAttributesData = SeriesCharacterRepo.fetchSeriesCharacterAttributesBySeriesId(userId, seriesId, lang); - const worldsData = SeriesWorldRepo.fetchSeriesWorldsTable(userId, seriesId, lang); - const worldElementsData = SeriesWorldRepo.fetchSeriesWorldElementsBySeriesId(userId, seriesId, lang); - const locationsData = SeriesLocationRepo.fetchSeriesLocationsTable(userId, seriesId, lang); - const locationElementsData = SeriesLocationRepo.fetchSeriesLocationElementsBySeriesId(userId, seriesId, lang); - const locationSubElementsData = SeriesLocationRepo.fetchSeriesLocationSubElementsBySeriesId(userId, seriesId, lang); - const spellsData = SeriesSpellRepo.fetchSeriesSpellsTable(userId, seriesId, lang); - const spellTagsData = SeriesSpellRepo.fetchSeriesSpellTagsTable(userId, seriesId, lang); - - // Decrypt series - const series: SeriesTable[] = seriesData.map((s: SeriesTable): SeriesTable => ({ - ...s, - name: System.decryptDataWithUserKey(s.name, userEncryptionKey), - description: s.description ? System.decryptDataWithUserKey(s.description, userEncryptionKey) : null, - cover_image: s.cover_image ? System.decryptDataWithUserKey(s.cover_image, userEncryptionKey) : null - })); - - // Decrypt characters - const seriesCharacters: SeriesCharactersTable[] = charactersData.map((c): SeriesCharactersTable => { - const decryptedAge: string | null = c.age ? System.decryptDataWithUserKey(c.age, userEncryptionKey) : null; - return { - character_id: c.character_id as string, - series_id: c.series_id as string, - user_id: c.user_id as string, - first_name: System.decryptDataWithUserKey(c.first_name as string, userEncryptionKey), - last_name: c.last_name ? System.decryptDataWithUserKey(c.last_name as string, userEncryptionKey) : null, - nickname: c.nickname ? System.decryptDataWithUserKey(c.nickname as string, userEncryptionKey) : null, - age: decryptedAge ? parseInt(decryptedAge, 10) : null, - gender: c.gender ? System.decryptDataWithUserKey(c.gender as string, userEncryptionKey) : null, - species: c.species ? System.decryptDataWithUserKey(c.species as string, userEncryptionKey) : null, - nationality: c.nationality ? System.decryptDataWithUserKey(c.nationality as string, userEncryptionKey) : null, - status: c.status ? System.decryptDataWithUserKey(c.status as string, userEncryptionKey) : null, - title: c.title ? System.decryptDataWithUserKey(c.title as string, userEncryptionKey) : null, - category: System.decryptDataWithUserKey(c.category as string, userEncryptionKey), - image: c.image ? System.decryptDataWithUserKey(c.image as string, userEncryptionKey) : null, - role: c.role ? System.decryptDataWithUserKey(c.role as string, userEncryptionKey) : null, - biography: c.biography ? System.decryptDataWithUserKey(c.biography as string, userEncryptionKey) : null, - history: c.history ? System.decryptDataWithUserKey(c.history as string, userEncryptionKey) : null, - speech_pattern: c.speech_pattern ? System.decryptDataWithUserKey(c.speech_pattern as string, userEncryptionKey) : null, - catchphrase: c.catchphrase ? System.decryptDataWithUserKey(c.catchphrase as string, userEncryptionKey) : null, - residence: c.residence ? System.decryptDataWithUserKey(c.residence as string, userEncryptionKey) : null, - notes: c.notes ? System.decryptDataWithUserKey(c.notes as string, userEncryptionKey) : null, - color: c.color ? System.decryptDataWithUserKey(c.color as string, userEncryptionKey) : null, - last_update: c.last_update as number - }; - }); - - // Decrypt character attributes - const seriesCharacterAttributes: SeriesCharacterAttributesTable[] = characterAttributesData.map((a: SeriesCharacterAttributesTable): SeriesCharacterAttributesTable => ({ - ...a, - attribute_name: System.decryptDataWithUserKey(a.attribute_name, userEncryptionKey), - attribute_value: System.decryptDataWithUserKey(a.attribute_value, userEncryptionKey) - })); - - // Decrypt worlds - const seriesWorlds: SeriesWorldsTable[] = worldsData.map((w: SeriesWorldsTable): SeriesWorldsTable => ({ - ...w, - name: System.decryptDataWithUserKey(w.name, userEncryptionKey), - history: w.history ? System.decryptDataWithUserKey(w.history, userEncryptionKey) : null, - politics: w.politics ? System.decryptDataWithUserKey(w.politics, userEncryptionKey) : null, - economy: w.economy ? System.decryptDataWithUserKey(w.economy, userEncryptionKey) : null, - religion: w.religion ? System.decryptDataWithUserKey(w.religion, userEncryptionKey) : null, - languages: w.languages ? System.decryptDataWithUserKey(w.languages, userEncryptionKey) : null - })); - - // Decrypt world elements - const seriesWorldElements: SeriesWorldElementsTable[] = worldElementsData.map((e: SeriesWorldElementsTable): SeriesWorldElementsTable => ({ - ...e, - name: System.decryptDataWithUserKey(e.name, userEncryptionKey), - description: e.description ? System.decryptDataWithUserKey(e.description, userEncryptionKey) : null - })); - - // Decrypt locations - const seriesLocations: SeriesLocationsTable[] = locationsData.map((l: SeriesLocationsTable): SeriesLocationsTable => ({ - ...l, - loc_name: System.decryptDataWithUserKey(l.loc_name, userEncryptionKey) - })); - - // Decrypt location elements - const seriesLocationElements: SeriesLocationElementsTable[] = locationElementsData.map((e: SeriesLocationElementsTable): SeriesLocationElementsTable => ({ - ...e, - element_name: System.decryptDataWithUserKey(e.element_name, userEncryptionKey), - element_description: e.element_description ? System.decryptDataWithUserKey(e.element_description, userEncryptionKey) : null - })); - - // Decrypt location sub-elements - const seriesLocationSubElements: SeriesLocationSubElementsTable[] = locationSubElementsData.map((se: SeriesLocationSubElementsTable): SeriesLocationSubElementsTable => ({ - ...se, - sub_elem_name: System.decryptDataWithUserKey(se.sub_elem_name, userEncryptionKey), - sub_elem_description: se.sub_elem_description ? System.decryptDataWithUserKey(se.sub_elem_description, userEncryptionKey) : null - })); - - // Decrypt spells - const seriesSpells: SeriesSpellsTable[] = spellsData.map((s): SeriesSpellsTable => ({ - spell_id: s.spell_id as string, - series_id: s.series_id as string, - user_id: s.user_id as string, - name: System.decryptDataWithUserKey(s.name as string, userEncryptionKey), - name_hash: s.name_hash as string, - description: s.description ? System.decryptDataWithUserKey(s.description as string, userEncryptionKey) : '', - appearance: s.appearance ? System.decryptDataWithUserKey(s.appearance as string, userEncryptionKey) : '', - tags: s.tags ? System.decryptDataWithUserKey(s.tags as string, userEncryptionKey) : '', - power_level: s.power_level ? System.decryptDataWithUserKey(s.power_level as string, userEncryptionKey) : null, - components: s.components ? System.decryptDataWithUserKey(s.components as string, userEncryptionKey) : null, - limitations: s.limitations ? System.decryptDataWithUserKey(s.limitations as string, userEncryptionKey) : null, - notes: s.notes ? System.decryptDataWithUserKey(s.notes as string, userEncryptionKey) : null, - last_update: s.last_update as number - })); - - // Decrypt spell tags - const seriesSpellTags: SeriesSpellTagsTable[] = spellTagsData.map((t: SeriesSpellTagsTable): SeriesSpellTagsTable => ({ - ...t, - name: System.decryptDataWithUserKey(t.name, userEncryptionKey) - })); - - return { - series, - seriesBooks: seriesBooksData, - seriesCharacters, - seriesCharacterAttributes, - seriesWorlds, - seriesWorldElements, - seriesLocations, - seriesLocationElements, - seriesLocationSubElements, - seriesSpells, - seriesSpellTags - }; - } - - /** - * Saves a complete series downloaded from the server to the local database. - * Encrypts all data before storing. - * @param userId - The unique identifier of the user - * @param completeSeries - The complete series data from the server - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if save was successful, false otherwise - */ - static saveCompleteSeries(userId: string, completeSeries: CompleteSeries, lang: 'fr' | 'en'): boolean { - const userEncryptionKey: string = getUserEncryptionKey(userId); - - // Save series - for (const series of completeSeries.series) { - const encryptedName: string = System.encryptDataWithUserKey(series.name, userEncryptionKey); - const encryptedDescription: string | null = series.description ? System.encryptDataWithUserKey(series.description, userEncryptionKey) : null; - const encryptedCoverImage: string | null = series.cover_image ? System.encryptDataWithUserKey(series.cover_image, userEncryptionKey) : null; - - const success: boolean = SeriesRepo.insertSyncSeries( - series.series_id, - userId, - encryptedName, - series.hashed_name, - encryptedDescription, - encryptedCoverImage, - series.last_update, - lang - ); - if (!success) return false; - } - - // Save series books (only if the book exists locally) - for (const seriesBook of completeSeries.seriesBooks) { - const bookExists: boolean = BookRepo.isBookExist(userId, seriesBook.book_id, lang); - if (!bookExists) continue; - - const success: boolean = SeriesRepo.insertSyncSeriesBook( - seriesBook.series_id, - seriesBook.book_id, - seriesBook.book_order, - seriesBook.last_update, - lang - ); - if (!success) return false; - } - - // Save characters - for (const character of completeSeries.seriesCharacters) { - const encFirstName: string = System.encryptDataWithUserKey(character.first_name, userEncryptionKey); - const encLastName: string | null = character.last_name ? System.encryptDataWithUserKey(character.last_name, userEncryptionKey) : null; - const encNickname: string | null = character.nickname ? System.encryptDataWithUserKey(character.nickname, userEncryptionKey) : null; - const encAge: string | null = character.age !== null ? System.encryptDataWithUserKey(String(character.age), userEncryptionKey) : null; - const encGender: string | null = character.gender ? System.encryptDataWithUserKey(character.gender, userEncryptionKey) : null; - const encSpecies: string | null = character.species ? System.encryptDataWithUserKey(character.species, userEncryptionKey) : null; - const encNationality: string | null = character.nationality ? System.encryptDataWithUserKey(character.nationality, userEncryptionKey) : null; - const encStatus: string | null = character.status ? System.encryptDataWithUserKey(character.status, userEncryptionKey) : null; - const encTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userEncryptionKey) : null; - const encCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey); - const encImage: string | null = character.image ? System.encryptDataWithUserKey(character.image, userEncryptionKey) : null; - const encRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userEncryptionKey) : null; - const encBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userEncryptionKey) : null; - const encHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userEncryptionKey) : null; - const encSpeechPattern: string | null = character.speech_pattern ? System.encryptDataWithUserKey(character.speech_pattern, userEncryptionKey) : null; - const encCatchphrase: string | null = character.catchphrase ? System.encryptDataWithUserKey(character.catchphrase, userEncryptionKey) : null; - const encResidence: string | null = character.residence ? System.encryptDataWithUserKey(character.residence, userEncryptionKey) : null; - const encNotes: string | null = character.notes ? System.encryptDataWithUserKey(character.notes, userEncryptionKey) : null; - const encColor: string | null = character.color ? System.encryptDataWithUserKey(character.color, userEncryptionKey) : null; - - const success: boolean = SeriesCharacterRepo.insertSyncSeriesCharacter( - character.character_id, - character.series_id, - userId, - encFirstName, - encLastName, - encNickname, - encAge, - encGender, - encSpecies, - encNationality, - encStatus, - encCategory, - encTitle, - encImage, - encRole, - encBiography, - encHistory, - encSpeechPattern, - encCatchphrase, - encResidence, - encNotes, - encColor, - character.last_update, - lang - ); - if (!success) return false; - } - - // Save character attributes - for (const attr of completeSeries.seriesCharacterAttributes) { - const encryptedName: string = System.encryptDataWithUserKey(attr.attribute_name, userEncryptionKey); - const encryptedValue: string = System.encryptDataWithUserKey(attr.attribute_value, userEncryptionKey); - - const success: boolean = SeriesCharacterRepo.insertSyncSeriesCharacterAttribute( - attr.attr_id, - attr.character_id, - userId, - encryptedName, - encryptedValue, - attr.last_update, - lang - ); - if (!success) return false; - } - - // Save worlds - for (const world of completeSeries.seriesWorlds) { - const encryptedName: string = System.encryptDataWithUserKey(world.name, userEncryptionKey); - const encryptedHistory: string | null = world.history ? System.encryptDataWithUserKey(world.history, userEncryptionKey) : null; - const encryptedPolitics: string | null = world.politics ? System.encryptDataWithUserKey(world.politics, userEncryptionKey) : null; - const encryptedEconomy: string | null = world.economy ? System.encryptDataWithUserKey(world.economy, userEncryptionKey) : null; - const encryptedReligion: string | null = world.religion ? System.encryptDataWithUserKey(world.religion, userEncryptionKey) : null; - const encryptedLanguages: string | null = world.languages ? System.encryptDataWithUserKey(world.languages, userEncryptionKey) : null; - - const success: boolean = SeriesWorldRepo.insertSyncSeriesWorld( - world.world_id, - world.series_id, - userId, - encryptedName, - world.hashed_name, - encryptedHistory, - encryptedPolitics, - encryptedEconomy, - encryptedReligion, - encryptedLanguages, - world.last_update, - lang - ); - if (!success) return false; - } - - // Save world elements - for (const element of completeSeries.seriesWorldElements) { - const encryptedName: string = System.encryptDataWithUserKey(element.name, userEncryptionKey); - const encryptedDescription: string | null = element.description ? System.encryptDataWithUserKey(element.description, userEncryptionKey) : null; - - const success: boolean = SeriesWorldRepo.insertSyncSeriesWorldElement( - element.element_id, - element.world_id, - userId, - element.element_type, - encryptedName, - element.original_name, - encryptedDescription, - element.last_update, - lang - ); - if (!success) return false; - } - - // Save locations - for (const location of completeSeries.seriesLocations) { - const encryptedName: string = System.encryptDataWithUserKey(location.loc_name, userEncryptionKey); - - const success: boolean = SeriesLocationRepo.insertSyncSeriesLocation( - location.loc_id, - location.series_id, - userId, - encryptedName, - location.loc_original_name, - location.last_update, - lang - ); - if (!success) return false; - } - - // Save location elements - for (const element of completeSeries.seriesLocationElements) { - const encryptedName: string = System.encryptDataWithUserKey(element.element_name, userEncryptionKey); - const encryptedDescription: string | null = element.element_description ? System.encryptDataWithUserKey(element.element_description, userEncryptionKey) : null; - - const success: boolean = SeriesLocationRepo.insertSyncSeriesLocationElement( - element.element_id, - element.location_id, - userId, - encryptedName, - element.original_name, - encryptedDescription, - element.last_update, - lang - ); - if (!success) return false; - } - - // Save location sub-elements - for (const subElement of completeSeries.seriesLocationSubElements) { - const encryptedName: string = System.encryptDataWithUserKey(subElement.sub_elem_name, userEncryptionKey); - const encryptedDescription: string | null = subElement.sub_elem_description ? System.encryptDataWithUserKey(subElement.sub_elem_description, userEncryptionKey) : null; - - const success: boolean = SeriesLocationRepo.insertSyncSeriesLocationSubElement( - subElement.sub_element_id, - subElement.element_id, - userId, - encryptedName, - subElement.original_name, - encryptedDescription, - subElement.last_update, - lang - ); - if (!success) return false; - } - - // Save spells - for (const spell of completeSeries.seriesSpells) { - const encryptedName: string = System.encryptDataWithUserKey(spell.name, userEncryptionKey); - const encryptedDescription: string = System.encryptDataWithUserKey(spell.description, userEncryptionKey); - const encryptedAppearance: string = System.encryptDataWithUserKey(spell.appearance, userEncryptionKey); - const encryptedTags: string = System.encryptDataWithUserKey(spell.tags, userEncryptionKey); - const encryptedPowerLevel: string | null = spell.power_level ? System.encryptDataWithUserKey(spell.power_level, userEncryptionKey) : null; - const encryptedComponents: string | null = spell.components ? System.encryptDataWithUserKey(spell.components, userEncryptionKey) : null; - const encryptedLimitations: string | null = spell.limitations ? System.encryptDataWithUserKey(spell.limitations, userEncryptionKey) : null; - const encryptedNotes: string | null = spell.notes ? System.encryptDataWithUserKey(spell.notes, userEncryptionKey) : null; - - const success: boolean = SeriesSpellRepo.insertSyncSeriesSpell( - spell.spell_id, - spell.series_id, - userId, - encryptedName, - spell.name_hash, - encryptedDescription, - encryptedAppearance, - encryptedTags, - encryptedPowerLevel, - encryptedComponents, - encryptedLimitations, - encryptedNotes, - spell.last_update, - lang - ); - if (!success) return false; - } - - // Save spell tags - for (const tag of completeSeries.seriesSpellTags) { - const encryptedName: string = System.encryptDataWithUserKey(tag.name, userEncryptionKey); - - const success: boolean = SeriesSpellRepo.insertSyncSeriesSpellTag( - tag.tag_id, - tag.series_id, - userId, - encryptedName, - tag.hashed_name, - tag.color, - tag.last_update, - lang - ); - if (!success) return false; - } - - return true; - } - - /** - * Synchronizes a series from server to client, updating existing records or inserting new ones. - * @param userId - The unique identifier of the user - * @param completeSeries - The complete series data from the server - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if sync was successful, false otherwise - */ - static syncSeriesFromServerToClient(userId: string, completeSeries: CompleteSeries, lang: 'fr' | 'en'): boolean { - const userEncryptionKey: string = getUserEncryptionKey(userId); - - // Sync series - for (const series of completeSeries.series) { - const encryptedName: string = System.encryptDataWithUserKey(series.name, userEncryptionKey); - const encryptedDescription: string | null = series.description ? System.encryptDataWithUserKey(series.description, userEncryptionKey) : null; - const encryptedCoverImage: string | null = series.cover_image ? System.encryptDataWithUserKey(series.cover_image, userEncryptionKey) : null; - - const exists: boolean = SeriesRepo.seriesExists(userId, series.series_id, lang); - if (exists) { - const success: boolean = SeriesRepo.updateSyncSeries( - userId, - series.series_id, - encryptedName, - series.hashed_name, - encryptedDescription, - encryptedCoverImage, - series.last_update, - lang - ); - if (!success) return false; - } else { - const success: boolean = SeriesRepo.insertSyncSeries( - series.series_id, - userId, - encryptedName, - series.hashed_name, - encryptedDescription, - encryptedCoverImage, - series.last_update, - lang - ); - if (!success) return false; - } - } - - // Sync series books (only if the book exists locally) - for (const seriesBook of completeSeries.seriesBooks) { - const bookExists: boolean = BookRepo.isBookExist(userId, seriesBook.book_id, lang); - if (!bookExists) continue; - - const success: boolean = SeriesRepo.insertSyncSeriesBook( - seriesBook.series_id, - seriesBook.book_id, - seriesBook.book_order, - seriesBook.last_update, - lang - ); - if (!success) return false; - } - - // Sync characters - for (const character of completeSeries.seriesCharacters) { - const encFirstName: string = System.encryptDataWithUserKey(character.first_name, userEncryptionKey); - const encLastName: string | null = character.last_name ? System.encryptDataWithUserKey(character.last_name, userEncryptionKey) : null; - const encNickname: string | null = character.nickname ? System.encryptDataWithUserKey(character.nickname, userEncryptionKey) : null; - const encAge: string | null = character.age !== null ? System.encryptDataWithUserKey(String(character.age), userEncryptionKey) : null; - const encGender: string | null = character.gender ? System.encryptDataWithUserKey(character.gender, userEncryptionKey) : null; - const encSpecies: string | null = character.species ? System.encryptDataWithUserKey(character.species, userEncryptionKey) : null; - const encNationality: string | null = character.nationality ? System.encryptDataWithUserKey(character.nationality, userEncryptionKey) : null; - const encStatus: string | null = character.status ? System.encryptDataWithUserKey(character.status, userEncryptionKey) : null; - const encTitle: string | null = character.title ? System.encryptDataWithUserKey(character.title, userEncryptionKey) : null; - const encCategory: string = System.encryptDataWithUserKey(character.category, userEncryptionKey); - const encImage: string | null = character.image ? System.encryptDataWithUserKey(character.image, userEncryptionKey) : null; - const encRole: string | null = character.role ? System.encryptDataWithUserKey(character.role, userEncryptionKey) : null; - const encBiography: string | null = character.biography ? System.encryptDataWithUserKey(character.biography, userEncryptionKey) : null; - const encHistory: string | null = character.history ? System.encryptDataWithUserKey(character.history, userEncryptionKey) : null; - const encSpeechPattern: string | null = character.speech_pattern ? System.encryptDataWithUserKey(character.speech_pattern, userEncryptionKey) : null; - const encCatchphrase: string | null = character.catchphrase ? System.encryptDataWithUserKey(character.catchphrase, userEncryptionKey) : null; - const encResidence: string | null = character.residence ? System.encryptDataWithUserKey(character.residence, userEncryptionKey) : null; - const encNotes: string | null = character.notes ? System.encryptDataWithUserKey(character.notes, userEncryptionKey) : null; - const encColor: string | null = character.color ? System.encryptDataWithUserKey(character.color, userEncryptionKey) : null; - - const exists: boolean = SeriesCharacterRepo.seriesCharacterExists(userId, character.character_id, lang); - if (exists) { - const success: boolean = SeriesCharacterRepo.updateSyncSeriesCharacter( - userId, - character.character_id, - encFirstName, - encLastName, - encNickname, - encAge, - encGender, - encSpecies, - encNationality, - encStatus, - encCategory, - encTitle, - encImage, - encRole, - encBiography, - encHistory, - encSpeechPattern, - encCatchphrase, - encResidence, - encNotes, - encColor, - character.last_update, - lang - ); - if (!success) return false; - } else { - const success: boolean = SeriesCharacterRepo.insertSyncSeriesCharacter( - character.character_id, - character.series_id, - userId, - encFirstName, - encLastName, - encNickname, - encAge, - encGender, - encSpecies, - encNationality, - encStatus, - encCategory, - encTitle, - encImage, - encRole, - encBiography, - encHistory, - encSpeechPattern, - encCatchphrase, - encResidence, - encNotes, - encColor, - character.last_update, - lang - ); - if (!success) return false; - } - } - - // Sync character attributes - for (const attr of completeSeries.seriesCharacterAttributes) { - const encryptedName: string = System.encryptDataWithUserKey(attr.attribute_name, userEncryptionKey); - const encryptedValue: string = System.encryptDataWithUserKey(attr.attribute_value, userEncryptionKey); - - const exists: boolean = SeriesCharacterRepo.seriesCharacterAttributeExists(userId, attr.attr_id, lang); - if (exists) { - const success: boolean = SeriesCharacterRepo.updateSyncSeriesCharacterAttribute( - userId, - attr.attr_id, - encryptedName, - encryptedValue, - attr.last_update, - lang - ); - if (!success) return false; - } else { - const success: boolean = SeriesCharacterRepo.insertSyncSeriesCharacterAttribute( - attr.attr_id, - attr.character_id, - userId, - encryptedName, - encryptedValue, - attr.last_update, - lang - ); - if (!success) return false; - } - } - - // Sync worlds - for (const world of completeSeries.seriesWorlds) { - const encryptedName: string = System.encryptDataWithUserKey(world.name, userEncryptionKey); - const encryptedHistory: string | null = world.history ? System.encryptDataWithUserKey(world.history, userEncryptionKey) : null; - const encryptedPolitics: string | null = world.politics ? System.encryptDataWithUserKey(world.politics, userEncryptionKey) : null; - const encryptedEconomy: string | null = world.economy ? System.encryptDataWithUserKey(world.economy, userEncryptionKey) : null; - const encryptedReligion: string | null = world.religion ? System.encryptDataWithUserKey(world.religion, userEncryptionKey) : null; - const encryptedLanguages: string | null = world.languages ? System.encryptDataWithUserKey(world.languages, userEncryptionKey) : null; - - const exists: boolean = SeriesWorldRepo.seriesWorldExists(userId, world.world_id, lang); - if (exists) { - const success: boolean = SeriesWorldRepo.updateSyncSeriesWorld( - world.world_id, - userId, - encryptedName, - encryptedHistory, - encryptedPolitics, - encryptedEconomy, - encryptedReligion, - encryptedLanguages, - world.last_update, - lang - ); - if (!success) return false; - } else { - const success: boolean = SeriesWorldRepo.insertSyncSeriesWorld( - world.world_id, - world.series_id, - userId, - encryptedName, - world.hashed_name, - encryptedHistory, - encryptedPolitics, - encryptedEconomy, - encryptedReligion, - encryptedLanguages, - world.last_update, - lang - ); - if (!success) return false; - } - } - - // Sync world elements - for (const element of completeSeries.seriesWorldElements) { - const encryptedName: string = System.encryptDataWithUserKey(element.name, userEncryptionKey); - const encryptedDescription: string | null = element.description ? System.encryptDataWithUserKey(element.description, userEncryptionKey) : null; - - const exists: boolean = SeriesWorldRepo.seriesWorldElementExists(userId, element.element_id, lang); - if (exists) { - const success: boolean = SeriesWorldRepo.updateSyncSeriesWorldElement( - element.element_id, - userId, - encryptedName, - encryptedDescription, - element.last_update, - lang - ); - if (!success) return false; - } else { - const success: boolean = SeriesWorldRepo.insertSyncSeriesWorldElement( - element.element_id, - element.world_id, - userId, - element.element_type, - encryptedName, - element.original_name, - encryptedDescription, - element.last_update, - lang - ); - if (!success) return false; - } - } - - // Sync locations - for (const location of completeSeries.seriesLocations) { - const encryptedName: string = System.encryptDataWithUserKey(location.loc_name, userEncryptionKey); - - const exists: boolean = SeriesLocationRepo.seriesLocationExists(userId, location.loc_id, lang); - if (exists) { - const success: boolean = SeriesLocationRepo.updateSyncSeriesLocation( - location.loc_id, - userId, - encryptedName, - location.last_update, - lang - ); - if (!success) return false; - } else { - const success: boolean = SeriesLocationRepo.insertSyncSeriesLocation( - location.loc_id, - location.series_id, - userId, - encryptedName, - location.loc_original_name, - location.last_update, - lang - ); - if (!success) return false; - } - } - - // Sync location elements - for (const element of completeSeries.seriesLocationElements) { - const encryptedName: string = System.encryptDataWithUserKey(element.element_name, userEncryptionKey); - const encryptedDescription: string | null = element.element_description ? System.encryptDataWithUserKey(element.element_description, userEncryptionKey) : null; - - const exists: boolean = SeriesLocationRepo.seriesLocationElementExists(userId, element.element_id, lang); - if (exists) { - const success: boolean = SeriesLocationRepo.updateSyncSeriesLocationElement( - element.element_id, - userId, - encryptedName, - encryptedDescription, - element.last_update, - lang - ); - if (!success) return false; - } else { - const success: boolean = SeriesLocationRepo.insertSyncSeriesLocationElement( - element.element_id, - element.location_id, - userId, - encryptedName, - element.original_name, - encryptedDescription, - element.last_update, - lang - ); - if (!success) return false; - } - } - - // Sync location sub-elements - for (const subElement of completeSeries.seriesLocationSubElements) { - const encryptedName: string = System.encryptDataWithUserKey(subElement.sub_elem_name, userEncryptionKey); - const encryptedDescription: string | null = subElement.sub_elem_description ? System.encryptDataWithUserKey(subElement.sub_elem_description, userEncryptionKey) : null; - - const exists: boolean = SeriesLocationRepo.seriesLocationSubElementExists(userId, subElement.sub_element_id, lang); - if (exists) { - const success: boolean = SeriesLocationRepo.updateSyncSeriesLocationSubElement( - subElement.sub_element_id, - userId, - encryptedName, - encryptedDescription, - subElement.last_update, - lang - ); - if (!success) return false; - } else { - const success: boolean = SeriesLocationRepo.insertSyncSeriesLocationSubElement( - subElement.sub_element_id, - subElement.element_id, - userId, - encryptedName, - subElement.original_name, - encryptedDescription, - subElement.last_update, - lang - ); - if (!success) return false; - } - } - - // Sync spells - for (const spell of completeSeries.seriesSpells) { - const encryptedName: string = System.encryptDataWithUserKey(spell.name, userEncryptionKey); - const encryptedDescription: string = System.encryptDataWithUserKey(spell.description, userEncryptionKey); - const encryptedAppearance: string = System.encryptDataWithUserKey(spell.appearance, userEncryptionKey); - const encryptedTags: string = System.encryptDataWithUserKey(spell.tags, userEncryptionKey); - const encryptedPowerLevel: string | null = spell.power_level ? System.encryptDataWithUserKey(spell.power_level, userEncryptionKey) : null; - const encryptedComponents: string | null = spell.components ? System.encryptDataWithUserKey(spell.components, userEncryptionKey) : null; - const encryptedLimitations: string | null = spell.limitations ? System.encryptDataWithUserKey(spell.limitations, userEncryptionKey) : null; - const encryptedNotes: string | null = spell.notes ? System.encryptDataWithUserKey(spell.notes, userEncryptionKey) : null; - - const exists: boolean = SeriesSpellRepo.seriesSpellExists(userId, spell.spell_id, lang); - if (exists) { - const success: boolean = SeriesSpellRepo.updateSyncSeriesSpell( - spell.spell_id, - userId, - encryptedName, - encryptedDescription, - encryptedAppearance, - encryptedTags, - encryptedPowerLevel, - encryptedComponents, - encryptedLimitations, - encryptedNotes, - spell.last_update, - lang - ); - if (!success) return false; - } else { - const success: boolean = SeriesSpellRepo.insertSyncSeriesSpell( - spell.spell_id, - spell.series_id, - userId, - encryptedName, - spell.name_hash, - encryptedDescription, - encryptedAppearance, - encryptedTags, - encryptedPowerLevel, - encryptedComponents, - encryptedLimitations, - encryptedNotes, - spell.last_update, - lang - ); - if (!success) return false; - } - } - - // Sync spell tags - for (const tag of completeSeries.seriesSpellTags) { - const encryptedName: string = System.encryptDataWithUserKey(tag.name, userEncryptionKey); - - const exists: boolean = SeriesSpellRepo.seriesSpellTagExists(userId, tag.tag_id, lang); - if (exists) { - const success: boolean = SeriesSpellRepo.updateSyncSeriesSpellTag( - tag.tag_id, - userId, - encryptedName, - tag.color, - tag.last_update, - lang - ); - if (!success) return false; - } else { - const success: boolean = SeriesSpellRepo.insertSyncSeriesSpellTag( - tag.tag_id, - tag.series_id, - userId, - encryptedName, - tag.hashed_name, - tag.color, - tag.last_update, - lang - ); - if (!success) return false; - } - } - - return true; - } -} diff --git a/electron/database/models/SeriesWorld.ts b/electron/database/models/SeriesWorld.ts deleted file mode 100644 index ffbeaa2..0000000 --- a/electron/database/models/SeriesWorld.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import SeriesWorldRepo, { SeriesWorldResult } from "../repositories/series-world.repo.js"; -import RemovedItem from "./RemovedItem.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 = { - 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 = 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 deletedAt - The timestamp of deletion - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if successful - */ - public static deleteElement(userId: string, elementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = SeriesWorldRepo.deleteElement(userId, elementId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, null, 'series_world_elements', elementId, deletedAt, lang); - } - return deleted; - } -} diff --git a/electron/database/models/Spell.ts b/electron/database/models/Spell.ts deleted file mode 100644 index 20c90ad..0000000 --- a/electron/database/models/Spell.ts +++ /dev/null @@ -1,377 +0,0 @@ -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'; -import RemovedItem from './RemovedItem.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; - seriesSpellId: string | null; -} - -export interface SpellListItem { - id: string; - name: string; - description: string; - tags: SpellTagProps[]; - seriesSpellId?: string | null; -} - -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, bookId: string, tagId: string, deletedAt: number = System.timeStampInSeconds(), 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 | null = spell.tags ? System.decryptDataWithUserKey(spell.tags, userKey) : null; - let tagsArray: string[] = []; - try { - tagsArray = decryptedTags ? 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 - const deleted: boolean = SpellTagRepo.deleteSpellTag(userId, tagId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'book_spell_tags', tagId, deletedAt, lang); - } - return deleted; - } - - /** - * 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 = 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 | null = spell.description ? System.decryptDataWithUserKey(spell.description, userKey) : null; - const decryptedTags: string | null = spell.tags ? System.decryptDataWithUserKey(spell.tags, userKey) : null; - - let tagIds: string[]; - try { - tagIds = decryptedTags ? 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 - ? (decryptedDescription.length > 150 ? decryptedDescription.substring(0, 150) + '...' : decryptedDescription) - : ''; - - return { - id: spell.spell_id, - name: decryptedName, - description: truncatedDescription, - tags: resolvedTags, - seriesSpellId: spell.series_spell_id || null, - }; - }); - - 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 | null = spell.description ? System.decryptDataWithUserKey(spell.description, userKey) : null; - const decryptedAppearance: string | null = spell.appearance ? System.decryptDataWithUserKey(spell.appearance, userKey) : null; - const decryptedTags: string | null = spell.tags ? System.decryptDataWithUserKey(spell.tags, userKey) : null; - - let tagIds: string[]; - try { - tagIds = decryptedTags ? 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, - seriesSpellId: spell.series_spell_id || 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', seriesSpellId: string | null = null): 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, - seriesSpellId, - ); - - return { - id: spellId, - name, - description, - appearance, - tags, - powerLevel, - components, - limitations, - notes, - seriesSpellId, - }; - } - - /** - * 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', seriesSpellId: string | null = null): 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, - seriesSpellId, - ); - } - - /** - * Deletes a spell. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param spellId - The unique identifier of the spell - * @param deletedAt - The timestamp of deletion - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the deletion was successful - */ - static deleteSpell(userId: string, bookId: string, spellId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = SpellRepo.deleteSpell(userId, spellId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'book_spells', spellId, deletedAt, lang); - } - return deleted; - } -} diff --git a/electron/database/models/Sync.ts b/electron/database/models/Sync.ts deleted file mode 100644 index 4159d8a..0000000 --- a/electron/database/models/Sync.ts +++ /dev/null @@ -1,1277 +0,0 @@ -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, - BookChaptersTable, - SyncedChapterInfoResult, - SyncedChapterResult -} from "../repositories/chapter.repository.js"; -import PlotPointRepository, { BookPlotPointsTable, SyncedPlotPointResult } from "../repositories/plotpoint.repository.js"; -import IncidentRepository, { BookIncidentsTable, SyncedIncidentResult } from "../repositories/incident.repository.js"; -import ChapterContentRepository, { - BookChapterContentTable, - SyncedChapterContentResult -} from "../repositories/chaptercontent.repository.js"; -import CharacterRepo, { - BookCharactersAttributesTable, - BookCharactersTable, - SyncedCharacterAttributeResult, - SyncedCharacterResult -} from "../repositories/character.repository.js"; -import LocationRepo, { - BookLocationTable, - LocationElementTable, - LocationSubElementTable, - SyncedLocationElementResult, - SyncedLocationResult, - SyncedLocationSubElementResult -} from "../repositories/location.repository.js"; -import WorldRepository, { - BookWorldElementsTable, - BookWorldTable, - SyncedWorldElementResult, - SyncedWorldResult -} from "../repositories/world.repository.js"; -import ActRepository, { - BookActSummariesTable, - SyncedActSummaryResult -} from "../repositories/act.repository.js"; -import GuidelineRepo, { - BookAIGuideLineTable, - BookGuideLineTable, - SyncedAIGuideLineResult, - SyncedGuideLineResult -} from "../repositories/guideline.repository.js"; -import IssueRepository, { BookIssuesTable, SyncedIssueResult } from "../repositories/issue.repository.js"; -import { SyncedChapter, SyncedChapterContent, SyncedChapterInfo } from "./Chapter.js"; -import { SyncedCharacter, SyncedCharacterAttribute } from "./Character.js"; -import { SyncedLocation, SyncedLocationElement, SyncedLocationSubElement } from "./Location.js"; -import { SyncedWorld, SyncedWorldElement } from "./World.js"; -import { SyncedIncident } from "./Incident.js"; -import { SyncedPlotPoint } from "./PlotPoint.js"; -import { SyncedIssue } from "./Issue.js"; -import { SyncedActSummary } from "./Act.js"; -import { SyncedAIGuideLine, SyncedGuideLine } from "./GuideLine.js"; -import { - SyncedSeries, - SyncedSeriesBook, - SyncedSeriesCharacter, - SyncedSeriesCharacterAttribute, - SyncedSeriesWorld, - SyncedSeriesWorldElement, - SyncedSeriesLocation, - SyncedSeriesLocationElement, - SyncedSeriesLocationSubElement, - SyncedSeriesSpell, - SyncedSeriesSpellTag -} from "./Book.js"; -import SeriesRepo, { - SyncedSeriesResult, - SyncedSeriesBookResult -} from "../repositories/series.repo.js"; -import SeriesCharacterRepo, { - SyncedSeriesCharacterResult, - SyncedSeriesCharacterAttributeResult -} from "../repositories/series-character.repo.js"; -import SeriesWorldRepo, { - SyncedSeriesWorldResult, - SyncedSeriesWorldElementResult -} from "../repositories/series-world.repo.js"; -import SeriesLocationRepo, { - SyncedSeriesLocationResult, - SyncedSeriesLocationElementResult, - SyncedSeriesLocationSubElementResult -} from "../repositories/series-location.repo.js"; -import SeriesSpellRepo, { - SyncedSeriesSpellResult, - SyncedSeriesSpellTagResult -} from "../repositories/series-spell.repo.js"; - -/** - * Handles synchronization operations between local database and remote server. - * Provides methods to fetch, compare, and sync book data including all related entities. - */ -export default class Sync { - /** - * Retrieves a complete book with all its associated entities for synchronization. - * Decrypts all encrypted fields using the user's encryption key. - * @param userId - The unique identifier of the user - * @param syncCompareData - Object containing IDs of entities to retrieve for sync comparison - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to a CompleteBook object with all decrypted data - */ - static async getCompleteSyncBook(userId: string, syncCompareData: BookSyncCompare, lang: "fr" | "en"): Promise { - const userEncryptionKey: string = getUserEncryptionKey(userId); - const decryptedBooks: EritBooksTable[] = []; - const decryptedChapters: BookChaptersTable[] = []; - const decryptedPlotPoints: BookPlotPointsTable[] = []; - const decryptedIncidents: BookIncidentsTable[] = []; - const decryptedChapterContents: BookChapterContentTable[] = []; - const decryptedChapterInfos: BookChapterInfosTable[] = []; - const decryptedCharacters: BookCharactersTable[] = []; - const decryptedCharacterAttributes: BookCharactersAttributesTable[] = []; - const decryptedLocations: BookLocationTable[] = []; - const decryptedLocationElements: LocationElementTable[] = []; - const decryptedLocationSubElements: LocationSubElementTable[] = []; - const decryptedWorlds: BookWorldTable[] = []; - const decryptedWorldElements: BookWorldElementsTable[] = []; - const decryptedActSummaries: BookActSummariesTable[] = []; - 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; - const plotPointIds: string[] = syncCompareData.plotPoints; - const incidentIds: string[] = syncCompareData.incidents; - const chapterContentIds: string[] = syncCompareData.chapterContents; - const chapterInfoIds: string[] = syncCompareData.chapterInfos; - const characterIds: string[] = syncCompareData.characters; - const characterAttributeIds: string[] = syncCompareData.characterAttributes; - const locationIds: string[] = syncCompareData.locations; - const locationElementIds: string[] = syncCompareData.locationElements; - const locationSubElementIds: string[] = syncCompareData.locationSubElements; - 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) { - const actSummaryResults: BookActSummariesTable[] = await ActRepository.fetchCompleteActSummaryById(actSummaryId, lang); - if (actSummaryResults.length > 0) { - const actSummaryRecord: BookActSummariesTable = actSummaryResults[0]; - decryptedActSummaries.push({ - ...actSummaryRecord, - summary: actSummaryRecord.summary ? System.decryptDataWithUserKey(actSummaryRecord.summary, userEncryptionKey) : null - }); - } - } - } - - if (chapterIds && chapterIds.length > 0) { - for (const chapterId of chapterIds) { - const chapterResults: BookChaptersTable[] = await ChapterRepo.fetchCompleteChapterById(chapterId, lang); - if (chapterResults.length > 0) { - const chapterRecord: BookChaptersTable = chapterResults[0]; - decryptedChapters.push({ - ...chapterRecord, - title: System.decryptDataWithUserKey(chapterRecord.title, userEncryptionKey) - }); - } - } - } - - if (plotPointIds && plotPointIds.length > 0) { - for (const plotPointId of plotPointIds) { - const plotPointResults: BookPlotPointsTable[] = await PlotPointRepository.fetchCompletePlotPointById(plotPointId, lang); - if (plotPointResults.length > 0) { - const plotPointRecord: BookPlotPointsTable = plotPointResults[0]; - decryptedPlotPoints.push({ - ...plotPointRecord, - title: System.decryptDataWithUserKey(plotPointRecord.title, userEncryptionKey), - summary: plotPointRecord.summary ? System.decryptDataWithUserKey(plotPointRecord.summary, userEncryptionKey) : null - }); - } - } - } - - if (incidentIds && incidentIds.length > 0) { - for (const incidentId of incidentIds) { - const incidentResults: BookIncidentsTable[] = await IncidentRepository.fetchCompleteIncidentById(incidentId, lang); - if (incidentResults.length > 0) { - const incidentRecord: BookIncidentsTable = incidentResults[0]; - decryptedIncidents.push({ - ...incidentRecord, - title: System.decryptDataWithUserKey(incidentRecord.title, userEncryptionKey), - summary: incidentRecord.summary ? System.decryptDataWithUserKey(incidentRecord.summary, userEncryptionKey) : null - }); - } - } - } - - if (chapterContentIds && chapterContentIds.length > 0) { - for (const chapterContentId of chapterContentIds) { - const chapterContentResults: BookChapterContentTable[] = await ChapterContentRepository.fetchCompleteChapterContentById(chapterContentId, lang); - if (chapterContentResults.length > 0) { - const chapterContentRecord: BookChapterContentTable = chapterContentResults[0]; - decryptedChapterContents.push({ - ...chapterContentRecord, - content: chapterContentRecord.content ? JSON.parse(System.decryptDataWithUserKey(chapterContentRecord.content, userEncryptionKey)) : null - }); - } - } - } - - if (chapterInfoIds && chapterInfoIds.length > 0) { - for (const chapterInfoId of chapterInfoIds) { - const chapterInfoResults: BookChapterInfosTable[] = await ChapterRepo.fetchCompleteChapterInfoById(chapterInfoId, lang); - if (chapterInfoResults.length > 0) { - const chapterInfoRecord: BookChapterInfosTable = chapterInfoResults[0]; - decryptedChapterInfos.push({ - ...chapterInfoRecord, - summary: chapterInfoRecord.summary ? System.decryptDataWithUserKey(chapterInfoRecord.summary, userEncryptionKey) : null, - goal: chapterInfoRecord.goal ? System.decryptDataWithUserKey(chapterInfoRecord.goal, userEncryptionKey) : null - }); - } - } - } - - if (characterIds && characterIds.length > 0) { - for (const characterId of characterIds) { - const characterResults: BookCharactersTable[] = await CharacterRepo.fetchCompleteCharacterById(characterId, lang); - if (characterResults.length > 0) { - const characterRecord: BookCharactersTable = characterResults[0]; - decryptedCharacters.push({ - ...characterRecord, - first_name: System.decryptDataWithUserKey(characterRecord.first_name, userEncryptionKey), - last_name: characterRecord.last_name ? System.decryptDataWithUserKey(characterRecord.last_name, userEncryptionKey) : null, - nickname: characterRecord.nickname ? System.decryptDataWithUserKey(characterRecord.nickname, userEncryptionKey) : null, - age: characterRecord.age ? System.decryptDataWithUserKey(characterRecord.age, userEncryptionKey) : null, - gender: characterRecord.gender ? System.decryptDataWithUserKey(characterRecord.gender, userEncryptionKey) : null, - species: characterRecord.species ? System.decryptDataWithUserKey(characterRecord.species, userEncryptionKey) : null, - nationality: characterRecord.nationality ? System.decryptDataWithUserKey(characterRecord.nationality, userEncryptionKey) : null, - status: characterRecord.status ? System.decryptDataWithUserKey(characterRecord.status, userEncryptionKey) : null, - category: System.decryptDataWithUserKey(characterRecord.category, userEncryptionKey), - title: characterRecord.title ? System.decryptDataWithUserKey(characterRecord.title, userEncryptionKey) : null, - role: characterRecord.role ? System.decryptDataWithUserKey(characterRecord.role, userEncryptionKey) : null, - biography: characterRecord.biography ? System.decryptDataWithUserKey(characterRecord.biography, userEncryptionKey) : null, - history: characterRecord.history ? System.decryptDataWithUserKey(characterRecord.history, userEncryptionKey) : null, - speech_pattern: characterRecord.speech_pattern ? System.decryptDataWithUserKey(characterRecord.speech_pattern, userEncryptionKey) : null, - catchphrase: characterRecord.catchphrase ? System.decryptDataWithUserKey(characterRecord.catchphrase, userEncryptionKey) : null, - residence: characterRecord.residence ? System.decryptDataWithUserKey(characterRecord.residence, userEncryptionKey) : null, - notes: characterRecord.notes ? System.decryptDataWithUserKey(characterRecord.notes, userEncryptionKey) : null, - color: characterRecord.color ? System.decryptDataWithUserKey(characterRecord.color, userEncryptionKey) : null - }); - } - } - } - - if (characterAttributeIds && characterAttributeIds.length > 0) { - for (const characterAttributeId of characterAttributeIds) { - const characterAttributeResults: BookCharactersAttributesTable[] = await CharacterRepo.fetchCompleteCharacterAttributeById(characterAttributeId, lang); - if (characterAttributeResults.length > 0) { - const characterAttributeRecord: BookCharactersAttributesTable = characterAttributeResults[0]; - decryptedCharacterAttributes.push({ - ...characterAttributeRecord, - attribute_name: System.decryptDataWithUserKey(characterAttributeRecord.attribute_name, userEncryptionKey), - attribute_value: System.decryptDataWithUserKey(characterAttributeRecord.attribute_value, userEncryptionKey) - }); - } - } - } - - if (locationIds && locationIds.length > 0) { - for (const locationId of locationIds) { - const locationResults: BookLocationTable[] = await LocationRepo.fetchCompleteLocationById(locationId, lang); - if (locationResults.length > 0) { - const locationRecord: BookLocationTable = locationResults[0]; - decryptedLocations.push({ - ...locationRecord, - loc_name: System.decryptDataWithUserKey(locationRecord.loc_name, userEncryptionKey) - }); - } - } - } - - if (locationElementIds && locationElementIds.length > 0) { - for (const locationElementId of locationElementIds) { - const locationElementResults: LocationElementTable[] = await LocationRepo.fetchCompleteLocationElementById(locationElementId, lang); - if (locationElementResults.length > 0) { - const locationElementRecord: LocationElementTable = locationElementResults[0]; - decryptedLocationElements.push({ - ...locationElementRecord, - element_name: System.decryptDataWithUserKey(locationElementRecord.element_name, userEncryptionKey), - element_description: locationElementRecord.element_description ? System.decryptDataWithUserKey(locationElementRecord.element_description, userEncryptionKey) : null - }); - } - } - } - - if (locationSubElementIds && locationSubElementIds.length > 0) { - for (const locationSubElementId of locationSubElementIds) { - const locationSubElementResults: LocationSubElementTable[] = await LocationRepo.fetchCompleteLocationSubElementById(locationSubElementId, lang); - if (locationSubElementResults.length > 0) { - const locationSubElementRecord: LocationSubElementTable = locationSubElementResults[0]; - decryptedLocationSubElements.push({ - ...locationSubElementRecord, - sub_elem_name: System.decryptDataWithUserKey(locationSubElementRecord.sub_elem_name, userEncryptionKey), - sub_elem_description: locationSubElementRecord.sub_elem_description ? System.decryptDataWithUserKey(locationSubElementRecord.sub_elem_description, userEncryptionKey) : null - }); - } - } - } - - if (worldIds && worldIds.length > 0) { - for (const worldId of worldIds) { - const worldResults: BookWorldTable[] = await WorldRepository.fetchCompleteWorldById(worldId, lang); - if (worldResults.length > 0) { - const worldRecord: BookWorldTable = worldResults[0]; - decryptedWorlds.push({ - ...worldRecord, - name: System.decryptDataWithUserKey(worldRecord.name, userEncryptionKey), - history: worldRecord.history ? System.decryptDataWithUserKey(worldRecord.history, userEncryptionKey) : null, - politics: worldRecord.politics ? System.decryptDataWithUserKey(worldRecord.politics, userEncryptionKey) : null, - economy: worldRecord.economy ? System.decryptDataWithUserKey(worldRecord.economy, userEncryptionKey) : null, - religion: worldRecord.religion ? System.decryptDataWithUserKey(worldRecord.religion, userEncryptionKey) : null, - languages: worldRecord.languages ? System.decryptDataWithUserKey(worldRecord.languages, userEncryptionKey) : null - }); - } - } - } - - if (worldElementIds && worldElementIds.length > 0) { - for (const worldElementId of worldElementIds) { - const worldElementResults: BookWorldElementsTable[] = await WorldRepository.fetchCompleteWorldElementById(worldElementId, lang); - if (worldElementResults.length > 0) { - const worldElementRecord: BookWorldElementsTable = worldElementResults[0]; - decryptedWorldElements.push({ - ...worldElementRecord, - name: System.decryptDataWithUserKey(worldElementRecord.name, userEncryptionKey), - description: worldElementRecord.description ? System.decryptDataWithUserKey(worldElementRecord.description, userEncryptionKey) : null - }); - } - } - } - - if (issueIds && issueIds.length > 0) { - for (const issueId of issueIds) { - const issueResults: BookIssuesTable[] = await IssueRepository.fetchCompleteIssueById(issueId, lang); - if (issueResults.length > 0) { - const issueRecord: BookIssuesTable = issueResults[0]; - decryptedIssues.push({ - ...issueRecord, - name: System.decryptDataWithUserKey(issueRecord.name, userEncryptionKey) - }); - } - } - } - - if (syncCompareData.guideLine) { - const guidelineResults: BookGuideLineTable[] = await GuidelineRepo.fetchBookGuideLineTable(userId, syncCompareData.id, lang); - if (guidelineResults.length > 0) { - const guidelineRecord: BookGuideLineTable = guidelineResults[0]; - decryptedGuideLines.push({ - ...guidelineRecord, - tone: guidelineRecord.tone ? System.decryptDataWithUserKey(guidelineRecord.tone, userEncryptionKey) : null, - atmosphere: guidelineRecord.atmosphere ? System.decryptDataWithUserKey(guidelineRecord.atmosphere, userEncryptionKey) : null, - writing_style: guidelineRecord.writing_style ? System.decryptDataWithUserKey(guidelineRecord.writing_style, userEncryptionKey) : null, - themes: guidelineRecord.themes ? System.decryptDataWithUserKey(guidelineRecord.themes, userEncryptionKey) : null, - symbolism: guidelineRecord.symbolism ? System.decryptDataWithUserKey(guidelineRecord.symbolism, userEncryptionKey) : null, - motifs: guidelineRecord.motifs ? System.decryptDataWithUserKey(guidelineRecord.motifs, userEncryptionKey) : null, - narrative_voice: guidelineRecord.narrative_voice ? System.decryptDataWithUserKey(guidelineRecord.narrative_voice, userEncryptionKey) : null, - pacing: guidelineRecord.pacing ? System.decryptDataWithUserKey(guidelineRecord.pacing, userEncryptionKey) : null, - intended_audience: guidelineRecord.intended_audience ? System.decryptDataWithUserKey(guidelineRecord.intended_audience, userEncryptionKey) : null, - key_messages: guidelineRecord.key_messages ? System.decryptDataWithUserKey(guidelineRecord.key_messages, userEncryptionKey) : null - }); - } - } - - if (syncCompareData.aiGuideLine) { - const aiGuidelineResults: BookAIGuideLineTable[] = await GuidelineRepo.fetchBookAIGuideLine(userId, syncCompareData.id, lang); - if (aiGuidelineResults.length > 0) { - const aiGuidelineRecord: BookAIGuideLineTable = aiGuidelineResults[0]; - decryptedAIGuideLines.push({ - ...aiGuidelineRecord, - global_resume: aiGuidelineRecord.global_resume ? System.decryptDataWithUserKey(aiGuidelineRecord.global_resume, userEncryptionKey) : null, - themes: aiGuidelineRecord.themes ? System.decryptDataWithUserKey(aiGuidelineRecord.themes, userEncryptionKey) : null, - tone: aiGuidelineRecord.tone ? System.decryptDataWithUserKey(aiGuidelineRecord.tone, userEncryptionKey) : null, - atmosphere: aiGuidelineRecord.atmosphere ? System.decryptDataWithUserKey(aiGuidelineRecord.atmosphere, userEncryptionKey) : null, - current_resume: aiGuidelineRecord.current_resume ? System.decryptDataWithUserKey(aiGuidelineRecord.current_resume, userEncryptionKey) : null - }); - } - } - - 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: spellRecord.description ? System.decryptDataWithUserKey(spellRecord.description, userEncryptionKey) : null, - appearance: spellRecord.appearance ? System.decryptDataWithUserKey(spellRecord.appearance, userEncryptionKey) : null, - tags: spellRecord.tags ? System.decryptDataWithUserKey(spellRecord.tags, userEncryptionKey) : null, - 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]; - decryptedBooks.push({ - ...bookRecord, - title: System.decryptDataWithUserKey(bookRecord.title, userEncryptionKey), - sub_title: bookRecord.sub_title ? System.decryptDataWithUserKey(bookRecord.sub_title, userEncryptionKey) : null, - summary: bookRecord.summary ? System.decryptDataWithUserKey(bookRecord.summary, userEncryptionKey) : null, - cover_image: bookRecord.cover_image ? System.decryptDataWithUserKey(bookRecord.cover_image, userEncryptionKey) : null - }); - } - - const bookToolsResult: BookToolsTable | null = BookRepo.fetchBookTools(userId, syncCompareData.id, lang); - const bookTools: BookToolsTable[] = bookToolsResult ? [bookToolsResult] : []; - - return { - eritBooks: decryptedBooks, - chapters: decryptedChapters, - plotPoints: decryptedPlotPoints, - incidents: decryptedIncidents, - chapterContents: decryptedChapterContents, - chapterInfos: decryptedChapterInfos, - characters: decryptedCharacters, - characterAttributes: decryptedCharacterAttributes, - locations: decryptedLocations, - locationElements: decryptedLocationElements, - locationSubElements: decryptedLocationSubElements, - worlds: decryptedWorlds, - worldElements: decryptedWorldElements, - actSummaries: decryptedActSummaries, - guideLine: decryptedGuideLines, - aiGuideLine: decryptedAIGuideLines, - issues: decryptedIssues, - bookTools: bookTools, - spells: decryptedSpells, - spellTags: decryptedSpellTags - }; - } - - /** - * Synchronizes a complete book from the server to the local client database. - * Encrypts all data before storing and handles both insert and update operations. - * @param userId - The unique identifier of the user - * @param completeBook - The complete book data received from the server - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to true if sync was successful, false otherwise - */ - static async syncBookFromServerToClient(userId: string, completeBook: CompleteBook, lang: "fr" | "en"): Promise { - const userEncryptionKey: string = getUserEncryptionKey(userId); - - const serverActSummaries: BookActSummariesTable[] = completeBook.actSummaries; - const serverChapters: BookChaptersTable[] = completeBook.chapters; - const serverPlotPoints: BookPlotPointsTable[] = completeBook.plotPoints; - const serverIncidents: BookIncidentsTable[] = completeBook.incidents; - const serverChapterContents: BookChapterContentTable[] = completeBook.chapterContents; - const serverChapterInfos: BookChapterInfosTable[] = completeBook.chapterInfos; - const serverCharacters: BookCharactersTable[] = completeBook.characters; - const serverCharacterAttributes: BookCharactersAttributesTable[] = completeBook.characterAttributes; - const serverLocations: BookLocationTable[] = completeBook.locations; - const serverLocationElements: LocationElementTable[] = completeBook.locationElements; - const serverLocationSubElements: LocationSubElementTable[] = completeBook.locationSubElements; - const serverWorlds: BookWorldTable[] = completeBook.worlds; - const serverWorldElements: BookWorldElementsTable[] = completeBook.worldElements; - const serverIssues: BookIssuesTable[] = completeBook.issues; - const serverGuideLines: BookGuideLineTable[] = completeBook.guideLine; - const serverAIGuideLines: BookAIGuideLineTable[] = completeBook.aiGuideLine; - - const bookId: string = completeBook.eritBooks.length > 0 ? completeBook.eritBooks[0].book_id : ''; - - if (serverChapters && serverChapters.length > 0) { - for (const serverChapter of serverChapters) { - const chapterExists: boolean = ChapterRepo.isChapterExist(userId, serverChapter.chapter_id, lang); - const encryptedTitle: string = System.encryptDataWithUserKey(serverChapter.title, userEncryptionKey); - if (chapterExists) { - const updateSuccessful: boolean = ChapterRepo.updateChapter(userId, serverChapter.chapter_id, encryptedTitle, serverChapter.hashed_title, serverChapter.chapter_order, serverChapter.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = ChapterRepo.insertSyncChapter(serverChapter.chapter_id, serverChapter.book_id, userId, encryptedTitle, serverChapter.hashed_title, serverChapter.words_count || 0, serverChapter.chapter_order, serverChapter.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverActSummaries && serverActSummaries.length > 0) { - for (const serverActSummary of serverActSummaries) { - const actSummaryExists: boolean = ActRepository.actSummarizeExist(userId, bookId, serverActSummary.act_index, lang); - const encryptedSummary: string = System.encryptDataWithUserKey(serverActSummary.summary ? serverActSummary.summary : '', userEncryptionKey); - if (actSummaryExists) { - const updateSuccessful: boolean = ActRepository.updateActSummary(userId, bookId, serverActSummary.act_index, encryptedSummary, serverActSummary.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = ActRepository.insertSyncActSummary(serverActSummary.act_sum_id, userId, bookId, serverActSummary.act_index, encryptedSummary, serverActSummary.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverPlotPoints && serverPlotPoints.length > 0) { - for (const serverPlotPoint of serverPlotPoints) { - const encryptedTitle: string = System.encryptDataWithUserKey(serverPlotPoint.title, userEncryptionKey); - const encryptedSummary: string = System.encryptDataWithUserKey(serverPlotPoint.summary ? serverPlotPoint.summary : '', userEncryptionKey); - const plotPointExists: boolean = PlotPointRepository.plotPointExist(userId, bookId, serverPlotPoint.plot_point_id, lang); - if (plotPointExists) { - const updateSuccessful: boolean = PlotPointRepository.updatePlotPoint(userId, bookId, serverPlotPoint.plot_point_id, encryptedTitle, serverPlotPoint.hashed_title, encryptedSummary, serverPlotPoint.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - if (!serverPlotPoint.linked_incident_id) { - return false; - } - const insertSuccessful: boolean = PlotPointRepository.insertSyncPlotPoint(serverPlotPoint.plot_point_id, encryptedTitle, serverPlotPoint.hashed_title, encryptedSummary, serverPlotPoint.linked_incident_id, serverPlotPoint.author_id, bookId, serverPlotPoint.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverIncidents && serverIncidents.length > 0) { - for (const serverIncident of serverIncidents) { - const encryptedTitle: string = System.encryptDataWithUserKey(serverIncident.title, userEncryptionKey); - const encryptedSummary: string = System.encryptDataWithUserKey(serverIncident.summary ? serverIncident.summary : '', userEncryptionKey); - const incidentExists: boolean = IncidentRepository.incidentExist(userId, bookId, serverIncident.incident_id, lang); - if (incidentExists) { - const updateSuccessful: boolean = IncidentRepository.updateIncident(userId, bookId, serverIncident.incident_id, encryptedTitle, serverIncident.hashed_title, encryptedSummary, serverIncident.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = IncidentRepository.insertSyncIncident(serverIncident.incident_id, userId, bookId, encryptedTitle, serverIncident.hashed_title, encryptedSummary, serverIncident.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverChapterContents && serverChapterContents.length > 0) { - for (const serverChapterContent of serverChapterContents) { - const chapterContentExists: boolean = ChapterContentRepository.isChapterContentExist(userId, serverChapterContent.content_id, lang); - const encryptedContent: string = System.encryptDataWithUserKey(serverChapterContent.content ? JSON.stringify(serverChapterContent.content) : '', userEncryptionKey); - if (chapterContentExists) { - const updateSuccessful: boolean = ChapterContentRepository.updateChapterContent(userId, serverChapterContent.chapter_id, serverChapterContent.version, encryptedContent, serverChapterContent.words_count, serverChapterContent.last_update); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = ChapterContentRepository.insertSyncChapterContent(serverChapterContent.content_id, serverChapterContent.chapter_id, userId, serverChapterContent.version, encryptedContent, serverChapterContent.words_count, serverChapterContent.time_on_it, serverChapterContent.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverChapterInfos && serverChapterInfos.length > 0) { - for (const serverChapterInfo of serverChapterInfos) { - const chapterInfoExists: boolean = ChapterRepo.isChapterInfoExist(userId, serverChapterInfo.chapter_id, lang); - const encryptedSummary: string = System.encryptDataWithUserKey(serverChapterInfo.summary ? serverChapterInfo.summary : '', userEncryptionKey); - const encryptedGoal: string = System.encryptDataWithUserKey(serverChapterInfo.goal ? serverChapterInfo.goal : '', userEncryptionKey); - if (chapterInfoExists) { - const updateSuccessful: boolean = ChapterRepo.updateChapterInfos(userId, serverChapterInfo.chapter_id, serverChapterInfo.act_id, bookId, serverChapterInfo.incident_id, serverChapterInfo.plot_point_id, encryptedSummary, encryptedGoal, serverChapterInfo.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = ChapterRepo.insertSyncChapterInfo(serverChapterInfo.chapter_info_id, serverChapterInfo.chapter_id, serverChapterInfo.act_id, serverChapterInfo.incident_id, serverChapterInfo.plot_point_id, bookId, serverChapterInfo.author_id, encryptedSummary, encryptedGoal, serverChapterInfo.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverCharacters && serverCharacters.length > 0) { - for (const serverCharacter of serverCharacters) { - const characterExists: boolean = CharacterRepo.isCharacterExist(userId, serverCharacter.character_id, lang); - const characterData = { - firstName: System.encryptDataWithUserKey(serverCharacter.first_name, userEncryptionKey), - lastName: System.encryptDataWithUserKey(serverCharacter.last_name ? serverCharacter.last_name : '', userEncryptionKey), - nickname: System.encryptDataWithUserKey(serverCharacter.nickname ? serverCharacter.nickname : '', userEncryptionKey), - age: System.encryptDataWithUserKey(serverCharacter.age ? serverCharacter.age : '', userEncryptionKey), - gender: System.encryptDataWithUserKey(serverCharacter.gender ? serverCharacter.gender : '', userEncryptionKey), - species: System.encryptDataWithUserKey(serverCharacter.species ? serverCharacter.species : '', userEncryptionKey), - nationality: System.encryptDataWithUserKey(serverCharacter.nationality ? serverCharacter.nationality : '', userEncryptionKey), - status: System.encryptDataWithUserKey(serverCharacter.status ? serverCharacter.status : 'alive', userEncryptionKey), - category: System.encryptDataWithUserKey(serverCharacter.category, userEncryptionKey), - title: System.encryptDataWithUserKey(serverCharacter.title ? serverCharacter.title : '', userEncryptionKey), - image: System.encryptDataWithUserKey(serverCharacter.image ? serverCharacter.image : '', userEncryptionKey), - role: System.encryptDataWithUserKey(serverCharacter.role ? serverCharacter.role : '', userEncryptionKey), - biography: System.encryptDataWithUserKey(serverCharacter.biography ? serverCharacter.biography : '', userEncryptionKey), - history: System.encryptDataWithUserKey(serverCharacter.history ? serverCharacter.history : '', userEncryptionKey), - speechPattern: System.encryptDataWithUserKey(serverCharacter.speech_pattern ? serverCharacter.speech_pattern : '', userEncryptionKey), - catchphrase: System.encryptDataWithUserKey(serverCharacter.catchphrase ? serverCharacter.catchphrase : '', userEncryptionKey), - residence: System.encryptDataWithUserKey(serverCharacter.residence ? serverCharacter.residence : '', userEncryptionKey), - notes: System.encryptDataWithUserKey(serverCharacter.notes ? serverCharacter.notes : '', userEncryptionKey), - color: System.encryptDataWithUserKey(serverCharacter.color ? serverCharacter.color : '', userEncryptionKey) - }; - if (characterExists) { - const updateSuccessful: boolean = CharacterRepo.updateCharacter(userId, serverCharacter.character_id, characterData, serverCharacter.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = CharacterRepo.insertSyncCharacter(serverCharacter.character_id, bookId, userId, characterData, serverCharacter.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverCharacterAttributes && serverCharacterAttributes.length > 0) { - for (const serverCharacterAttribute of serverCharacterAttributes) { - const characterAttributeExists: boolean = CharacterRepo.isCharacterAttributeExist(userId, serverCharacterAttribute.attr_id, lang); - const encryptedAttributeName: string = System.encryptDataWithUserKey(serverCharacterAttribute.attribute_name, userEncryptionKey); - const encryptedAttributeValue: string = System.encryptDataWithUserKey(serverCharacterAttribute.attribute_value, userEncryptionKey); - if (characterAttributeExists) { - const updateSuccessful: boolean = CharacterRepo.updateCharacterAttribute(userId, serverCharacterAttribute.attr_id, encryptedAttributeName, encryptedAttributeValue, serverCharacterAttribute.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = CharacterRepo.insertSyncCharacterAttribute(serverCharacterAttribute.attr_id, serverCharacterAttribute.character_id, userId, encryptedAttributeName, encryptedAttributeValue, serverCharacterAttribute.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverLocations && serverLocations.length > 0) { - for (const serverLocation of serverLocations) { - const locationExists: boolean = LocationRepo.isLocationExist(userId, serverLocation.loc_id, lang); - const encryptedLocationName: string = System.encryptDataWithUserKey(serverLocation.loc_name, userEncryptionKey); - if (locationExists) { - const updateSuccessful: boolean = LocationRepo.updateLocationSection(userId, serverLocation.loc_id, encryptedLocationName, serverLocation.loc_original_name, serverLocation.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = LocationRepo.insertSyncLocation(serverLocation.loc_id, bookId, userId, encryptedLocationName, serverLocation.loc_original_name, serverLocation.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverLocationElements && serverLocationElements.length > 0) { - for (const serverLocationElement of serverLocationElements) { - const locationElementExists: boolean = LocationRepo.isLocationElementExist(userId, serverLocationElement.element_id, lang); - const encryptedElementName: string = System.encryptDataWithUserKey(serverLocationElement.element_name, userEncryptionKey); - const encryptedElementDescription: string = System.encryptDataWithUserKey(serverLocationElement.element_description ? serverLocationElement.element_description : '', userEncryptionKey); - if (locationElementExists) { - const updateSuccessful: boolean = LocationRepo.updateLocationElement(userId, serverLocationElement.element_id, encryptedElementName, serverLocationElement.original_name, encryptedElementDescription, serverLocationElement.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = LocationRepo.insertSyncLocationElement(serverLocationElement.element_id, serverLocationElement.location, userId, encryptedElementName, serverLocationElement.original_name, encryptedElementDescription, serverLocationElement.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverLocationSubElements && serverLocationSubElements.length > 0) { - for (const serverLocationSubElement of serverLocationSubElements) { - const locationSubElementExists: boolean = LocationRepo.isLocationSubElementExist(userId, serverLocationSubElement.sub_element_id, lang); - const encryptedSubElementName: string = System.encryptDataWithUserKey(serverLocationSubElement.sub_elem_name, userEncryptionKey); - const encryptedSubElementDescription: string = System.encryptDataWithUserKey(serverLocationSubElement.sub_elem_description ? serverLocationSubElement.sub_elem_description : '', userEncryptionKey); - if (locationSubElementExists) { - const updateSuccessful: boolean = LocationRepo.updateLocationSubElement(userId, serverLocationSubElement.sub_element_id, encryptedSubElementName, serverLocationSubElement.original_name, encryptedSubElementDescription, serverLocationSubElement.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = LocationRepo.insertSyncLocationSubElement(serverLocationSubElement.sub_element_id, serverLocationSubElement.element_id, userId, encryptedSubElementName, serverLocationSubElement.original_name, encryptedSubElementDescription, serverLocationSubElement.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverWorlds && serverWorlds.length > 0) { - for (const serverWorld of serverWorlds) { - const worldExists: boolean = WorldRepository.worldExist(userId, bookId, serverWorld.world_id, lang); - const encryptedName: string = System.encryptDataWithUserKey(serverWorld.name, userEncryptionKey); - const encryptedHistory: string = System.encryptDataWithUserKey(serverWorld.history ? serverWorld.history : '', userEncryptionKey); - const encryptedPolitics: string = System.encryptDataWithUserKey(serverWorld.politics ? serverWorld.politics : '', userEncryptionKey); - const encryptedEconomy: string = System.encryptDataWithUserKey(serverWorld.economy ? serverWorld.economy : '', userEncryptionKey); - const encryptedReligion: string = System.encryptDataWithUserKey(serverWorld.religion ? serverWorld.religion : '', userEncryptionKey); - const encryptedLanguages: string = System.encryptDataWithUserKey(serverWorld.languages ? serverWorld.languages : '', userEncryptionKey); - if (worldExists) { - const updateSuccessful: boolean = WorldRepository.updateWorld(userId, serverWorld.world_id, encryptedName, serverWorld.hashed_name, encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, serverWorld.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = WorldRepository.insertSyncWorld(serverWorld.world_id, encryptedName, serverWorld.hashed_name, userId, bookId, encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, serverWorld.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverWorldElements && serverWorldElements.length > 0) { - for (const serverWorldElement of serverWorldElements) { - const worldElementExists: boolean = WorldRepository.worldElementExist(userId, serverWorldElement.world_id, serverWorldElement.element_id, lang); - const encryptedName: string = System.encryptDataWithUserKey(serverWorldElement.name, userEncryptionKey); - const encryptedDescription: string = System.encryptDataWithUserKey(serverWorldElement.description ? serverWorldElement.description : '', userEncryptionKey); - if (worldElementExists) { - const updateSuccessful: boolean = WorldRepository.updateWorldElement(userId, serverWorldElement.element_id, encryptedName, encryptedDescription, serverWorldElement.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = WorldRepository.insertSyncWorldElement(serverWorldElement.element_id, serverWorldElement.world_id, userId, serverWorldElement.element_type, encryptedName, serverWorldElement.original_name, encryptedDescription, serverWorldElement.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverIssues && serverIssues.length > 0) { - for (const serverIssue of serverIssues) { - const issueExists: boolean = IssueRepository.issueExist(userId, bookId, serverIssue.issue_id, lang); - const encryptedName: string = System.encryptDataWithUserKey(serverIssue.name, userEncryptionKey); - if (issueExists) { - const updateSuccessful: boolean = IssueRepository.updateIssue(userId, bookId, serverIssue.issue_id, encryptedName, serverIssue.hashed_issue_name, serverIssue.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = IssueRepository.insertSyncIssue(serverIssue.issue_id, userId, bookId, encryptedName, serverIssue.hashed_issue_name, serverIssue.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverGuideLines && serverGuideLines.length > 0) { - for (const serverGuideLine of serverGuideLines) { - const guideLineExists: boolean = GuidelineRepo.guideLineExist(userId, bookId, lang); - const encryptedTone: string | null = serverGuideLine.tone ? System.encryptDataWithUserKey(serverGuideLine.tone, userEncryptionKey) : null; - const encryptedAtmosphere: string | null = serverGuideLine.atmosphere ? System.encryptDataWithUserKey(serverGuideLine.atmosphere, userEncryptionKey) : null; - const encryptedWritingStyle: string | null = serverGuideLine.writing_style ? System.encryptDataWithUserKey(serverGuideLine.writing_style, userEncryptionKey) : null; - const encryptedThemes: string | null = serverGuideLine.themes ? System.encryptDataWithUserKey(serverGuideLine.themes, userEncryptionKey) : null; - const encryptedSymbolism: string | null = serverGuideLine.symbolism ? System.encryptDataWithUserKey(serverGuideLine.symbolism, userEncryptionKey) : null; - const encryptedMotifs: string | null = serverGuideLine.motifs ? System.encryptDataWithUserKey(serverGuideLine.motifs, userEncryptionKey) : null; - const encryptedNarrativeVoice: string | null = serverGuideLine.narrative_voice ? System.encryptDataWithUserKey(serverGuideLine.narrative_voice, userEncryptionKey) : null; - const encryptedPacing: string | null = serverGuideLine.pacing ? System.encryptDataWithUserKey(serverGuideLine.pacing, userEncryptionKey) : null; - const encryptedKeyMessages: string | null = serverGuideLine.key_messages ? System.encryptDataWithUserKey(serverGuideLine.key_messages, userEncryptionKey) : null; - const encryptedIntendedAudience: string | null = serverGuideLine.intended_audience ? System.encryptDataWithUserKey(serverGuideLine.intended_audience, userEncryptionKey) : null; - if (guideLineExists) { - const updateSuccessful: boolean = GuidelineRepo.updateGuideLine(userId, bookId, encryptedTone, encryptedAtmosphere, encryptedWritingStyle, encryptedThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedKeyMessages, encryptedIntendedAudience, serverGuideLine.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = GuidelineRepo.insertSyncGuideLine(userId, bookId, encryptedTone, encryptedAtmosphere, encryptedWritingStyle, encryptedThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedIntendedAudience, encryptedKeyMessages, serverGuideLine.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (serverAIGuideLines && serverAIGuideLines.length > 0) { - for (const serverAIGuideLine of serverAIGuideLines) { - const aiGuideLineExists: boolean = GuidelineRepo.aiGuideLineExist(userId, bookId, lang); - const encryptedGlobalResume: string | null = serverAIGuideLine.global_resume ? System.encryptDataWithUserKey(serverAIGuideLine.global_resume, userEncryptionKey) : null; - const encryptedThemes: string | null = serverAIGuideLine.themes ? System.encryptDataWithUserKey(serverAIGuideLine.themes, userEncryptionKey) : null; - const encryptedTone: string | null = serverAIGuideLine.tone ? System.encryptDataWithUserKey(serverAIGuideLine.tone, userEncryptionKey) : null; - const encryptedAtmosphere: string | null = serverAIGuideLine.atmosphere ? System.encryptDataWithUserKey(serverAIGuideLine.atmosphere, userEncryptionKey) : null; - const encryptedCurrentResume: string | null = serverAIGuideLine.current_resume ? System.encryptDataWithUserKey(serverAIGuideLine.current_resume, userEncryptionKey) : null; - if (aiGuideLineExists) { - const updateSuccessful: boolean = GuidelineRepo.insertAIGuideLine(userId, bookId, serverAIGuideLine.narrative_type, serverAIGuideLine.dialogue_type, encryptedGlobalResume, encryptedAtmosphere, serverAIGuideLine.verbe_tense, serverAIGuideLine.langue, encryptedThemes, serverAIGuideLine.last_update, lang); - if (!updateSuccessful) { - return false; - } - } else { - const insertSuccessful: boolean = GuidelineRepo.insertSyncAIGuideLine(userId, bookId, encryptedGlobalResume, encryptedThemes, serverAIGuideLine.verbe_tense, serverAIGuideLine.narrative_type, serverAIGuideLine.langue, serverAIGuideLine.dialogue_type, encryptedTone, encryptedAtmosphere, encryptedCurrentResume, serverAIGuideLine.last_update, lang); - if (!insertSuccessful) { - return false; - } - } - } - } - - if (completeBook.bookTools && completeBook.bookTools.length > 0) { - for (const serverBookTool of completeBook.bookTools) { - 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 | null = serverSpell.description ? System.encryptDataWithUserKey(serverSpell.description, userEncryptionKey) : null; - const encryptedAppearance: string | null = serverSpell.appearance ? System.encryptDataWithUserKey(serverSpell.appearance, userEncryptionKey) : null; - const encryptedTags: string | null = serverSpell.tags ? System.encryptDataWithUserKey(serverSpell.tags, userEncryptionKey) : null; - 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; - } - - /** - * Retrieves all synced books for a user with their complete hierarchical data structure. - * Fetches all related entities (chapters, characters, locations, etc.) and organizes them by book. - * @param userId - The unique identifier of the user - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of SyncedBook objects with decrypted data - */ - static async getSyncedBooks(userId: string, lang: 'fr' | 'en'): Promise { - const userEncryptionKey: string = getUserEncryptionKey(userId); - - const [ - allBooks, - allChapters, - allChapterContents, - allChapterInfos, - allCharacters, - allCharacterAttributes, - allLocations, - allLocationElements, - allLocationSubElements, - allWorlds, - allWorldElements, - allIncidents, - allPlotPoints, - allIssues, - allActSummaries, - allGuidelines, - allAIGuidelines, - allSpells, - allSpellTags - ]: [ - SyncedBookResult[], - SyncedChapterResult[], - SyncedChapterContentResult[], - SyncedChapterInfoResult[], - SyncedCharacterResult[], - SyncedCharacterAttributeResult[], - SyncedLocationResult[], - SyncedLocationElementResult[], - SyncedLocationSubElementResult[], - SyncedWorldResult[], - SyncedWorldElementResult[], - SyncedIncidentResult[], - SyncedPlotPointResult[], - SyncedIssueResult[], - SyncedActSummaryResult[], - SyncedGuideLineResult[], - SyncedAIGuideLineResult[], - SyncedSpellResult[], - SyncedSpellTagResult[] - ] = await Promise.all([ - BookRepo.fetchSyncedBooks(userId, lang), - ChapterRepo.fetchSyncedChapters(userId, lang), - ChapterContentRepository.fetchSyncedChapterContents(userId, lang), - ChapterRepo.fetchSyncedChapterInfos(userId, lang), - CharacterRepo.fetchSyncedCharacters(userId, lang), - CharacterRepo.fetchSyncedCharacterAttributes(userId, lang), - LocationRepo.fetchSyncedLocations(userId, lang), - LocationRepo.fetchSyncedLocationElements(userId, lang), - LocationRepo.fetchSyncedLocationSubElements(userId, lang), - WorldRepository.fetchSyncedWorlds(userId, lang), - WorldRepository.fetchSyncedWorldElements(userId, lang), - IncidentRepository.fetchSyncedIncidents(userId, lang), - PlotPointRepository.fetchSyncedPlotPoints(userId, lang), - IssueRepository.fetchSyncedIssues(userId, lang), - ActRepository.fetchSyncedActSummaries(userId, lang), - GuidelineRepo.fetchSyncedGuideLine(userId, lang), - GuidelineRepo.fetchSyncedAIGuideLine(userId, lang), - SpellRepo.fetchSyncedSpells(userId, lang), - SpellTagRepo.fetchSyncedSpellTags(userId, lang) - ]); - - return allBooks.map((bookRecord: SyncedBookResult): SyncedBook => { - const currentBookId: string = bookRecord.book_id; - - const bookChapters: SyncedChapter[] = allChapters - .filter((chapterRecord: SyncedChapterResult): boolean => chapterRecord.book_id === currentBookId) - .map((chapterRecord: SyncedChapterResult): SyncedChapter => { - const currentChapterId: string = chapterRecord.chapter_id; - - const chapterContents: SyncedChapterContent[] = allChapterContents - .filter((contentRecord: SyncedChapterContentResult): boolean => contentRecord.chapter_id === currentChapterId) - .map((contentRecord: SyncedChapterContentResult): SyncedChapterContent => ({ - id: contentRecord.content_id, - lastUpdate: contentRecord.last_update - })); - - const chapterInfoRecord: SyncedChapterInfoResult | undefined = allChapterInfos.find((infoRecord: SyncedChapterInfoResult): boolean => infoRecord.chapter_id === currentChapterId); - const chapterInfo: SyncedChapterInfo | null = chapterInfoRecord ? { - id: chapterInfoRecord.chapter_info_id, - lastUpdate: chapterInfoRecord.last_update - } : null; - - return { - id: currentChapterId, - name: System.decryptDataWithUserKey(chapterRecord.title, userEncryptionKey), - lastUpdate: chapterRecord.last_update, - contents: chapterContents, - info: chapterInfo - }; - }); - - const bookCharacters: SyncedCharacter[] = allCharacters - .filter((characterRecord: SyncedCharacterResult): boolean => characterRecord.book_id === currentBookId) - .map((characterRecord: SyncedCharacterResult): SyncedCharacter => { - const currentCharacterId: string = characterRecord.character_id; - - const characterAttributes: SyncedCharacterAttribute[] = allCharacterAttributes - .filter((attributeRecord: SyncedCharacterAttributeResult): boolean => attributeRecord.character_id === currentCharacterId) - .map((attributeRecord: SyncedCharacterAttributeResult): SyncedCharacterAttribute => ({ - id: attributeRecord.attr_id, - name: System.decryptDataWithUserKey(attributeRecord.attribute_name, userEncryptionKey), - lastUpdate: attributeRecord.last_update - })); - - return { - id: currentCharacterId, - name: System.decryptDataWithUserKey(characterRecord.first_name, userEncryptionKey), - lastUpdate: characterRecord.last_update, - attributes: characterAttributes - }; - }); - - const bookLocations: SyncedLocation[] = allLocations - .filter((locationRecord: SyncedLocationResult): boolean => locationRecord.book_id === currentBookId) - .map((locationRecord: SyncedLocationResult): SyncedLocation => { - const currentLocationId: string = locationRecord.loc_id; - - const locationElements: SyncedLocationElement[] = allLocationElements - .filter((elementRecord: SyncedLocationElementResult): boolean => elementRecord.location === currentLocationId) - .map((elementRecord: SyncedLocationElementResult): SyncedLocationElement => { - const currentElementId: string = elementRecord.element_id; - - const locationSubElements: SyncedLocationSubElement[] = allLocationSubElements - .filter((subElementRecord: SyncedLocationSubElementResult): boolean => subElementRecord.element_id === currentElementId) - .map((subElementRecord: SyncedLocationSubElementResult): SyncedLocationSubElement => ({ - id: subElementRecord.sub_element_id, - name: System.decryptDataWithUserKey(subElementRecord.sub_elem_name, userEncryptionKey), - lastUpdate: subElementRecord.last_update - })); - - return { - id: currentElementId, - name: System.decryptDataWithUserKey(elementRecord.element_name, userEncryptionKey), - lastUpdate: elementRecord.last_update, - subElements: locationSubElements - }; - }); - - return { - id: currentLocationId, - name: System.decryptDataWithUserKey(locationRecord.loc_name, userEncryptionKey), - lastUpdate: locationRecord.last_update, - elements: locationElements - }; - }); - - const bookWorlds: SyncedWorld[] = allWorlds - .filter((worldRecord: SyncedWorldResult): boolean => worldRecord.book_id === currentBookId) - .map((worldRecord: SyncedWorldResult): SyncedWorld => { - const currentWorldId: string = worldRecord.world_id; - - const worldElements: SyncedWorldElement[] = allWorldElements - .filter((worldElementRecord: SyncedWorldElementResult): boolean => worldElementRecord.world_id === currentWorldId) - .map((worldElementRecord: SyncedWorldElementResult): SyncedWorldElement => ({ - id: worldElementRecord.element_id, - name: System.decryptDataWithUserKey(worldElementRecord.name, userEncryptionKey), - lastUpdate: worldElementRecord.last_update - })); - - return { - id: currentWorldId, - name: System.decryptDataWithUserKey(worldRecord.name, userEncryptionKey), - lastUpdate: worldRecord.last_update, - elements: worldElements - }; - }); - - const bookIncidents: SyncedIncident[] = allIncidents - .filter((incidentRecord: SyncedIncidentResult): boolean => incidentRecord.book_id === currentBookId) - .map((incidentRecord: SyncedIncidentResult): SyncedIncident => ({ - id: incidentRecord.incident_id, - name: System.decryptDataWithUserKey(incidentRecord.title, userEncryptionKey), - lastUpdate: incidentRecord.last_update - })); - - const bookPlotPoints: SyncedPlotPoint[] = allPlotPoints - .filter((plotPointRecord: SyncedPlotPointResult): boolean => plotPointRecord.book_id === currentBookId) - .map((plotPointRecord: SyncedPlotPointResult): SyncedPlotPoint => ({ - id: plotPointRecord.plot_point_id, - name: System.decryptDataWithUserKey(plotPointRecord.title, userEncryptionKey), - lastUpdate: plotPointRecord.last_update - })); - - const bookIssues: SyncedIssue[] = allIssues - .filter((issueRecord: SyncedIssueResult): boolean => issueRecord.book_id === currentBookId) - .map((issueRecord: SyncedIssueResult): SyncedIssue => ({ - id: issueRecord.issue_id, - name: System.decryptDataWithUserKey(issueRecord.name, userEncryptionKey), - lastUpdate: issueRecord.last_update - })); - - const bookActSummaries: SyncedActSummary[] = allActSummaries - .filter((actSummaryRecord: SyncedActSummaryResult): boolean => actSummaryRecord.book_id === currentBookId) - .map((actSummaryRecord: SyncedActSummaryResult): SyncedActSummary => ({ - id: actSummaryRecord.act_sum_id, - lastUpdate: actSummaryRecord.last_update - })); - - const guidelineRecord: SyncedGuideLineResult | undefined = allGuidelines.find((guidelineItem: SyncedGuideLineResult): boolean => guidelineItem.book_id === currentBookId); - const bookGuideLine: SyncedGuideLine | null = guidelineRecord ? { - lastUpdate: guidelineRecord.last_update - } : null; - - const aiGuidelineRecord: SyncedAIGuideLineResult | undefined = allAIGuidelines.find((aiGuidelineItem: SyncedAIGuideLineResult): boolean => aiGuidelineItem.book_id === currentBookId); - const bookAIGuideLine: SyncedAIGuideLine | null = aiGuidelineRecord ? { - lastUpdate: aiGuidelineRecord.last_update - } : null; - - const bookToolsQuery: SyncedBookToolsResult | null = BookRepo.fetchSyncedBookTools(userId, currentBookId, lang); - const bookTools: SyncedBookTools | null = bookToolsQuery ? { - 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 - .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, - title: System.decryptDataWithUserKey(bookRecord.title, userEncryptionKey), - subTitle: bookRecord.sub_title ? System.decryptDataWithUserKey(bookRecord.sub_title, userEncryptionKey) : null, - lastUpdate: bookRecord.last_update, - chapters: bookChapters, - characters: bookCharacters, - locations: bookLocations, - worlds: bookWorlds, - incidents: bookIncidents, - plotPoints: bookPlotPoints, - issues: bookIssues, - actSummaries: bookActSummaries, - guideLine: bookGuideLine, - aiGuideLine: bookAIGuideLine, - bookTools: bookTools, - spells: bookSpells, - spellTags: bookSpellTags - }; - }); - } - - // ===== SERIES SYNC METHODS ===== - - /** - * Retrieves all series for the current user with lightweight structure for sync comparison. - * Returns synced series with nested characters, worlds, locations, spells, and spell tags. - * All encrypted fields are decrypted before return. - * @param userId - The unique identifier of the user - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of synced series with all nested entities - */ - static getSyncedSeries(userId: string, lang: 'fr' | 'en'): SyncedSeries[] { - const userEncryptionKey: string = getUserEncryptionKey(userId); - - const allSeries: SyncedSeriesResult[] = SeriesRepo.fetchSyncedSeries(userId, lang); - const allSeriesBooks: SyncedSeriesBookResult[] = SeriesRepo.fetchSyncedSeriesBooks(userId, lang); - const allCharacters: SyncedSeriesCharacterResult[] = SeriesCharacterRepo.fetchSyncedSeriesCharacters(userId, lang); - const allCharacterAttributes: SyncedSeriesCharacterAttributeResult[] = SeriesCharacterRepo.fetchSyncedSeriesCharacterAttributes(userId, lang); - const allWorlds: SyncedSeriesWorldResult[] = SeriesWorldRepo.fetchSyncedSeriesWorlds(userId, lang); - const allWorldElements: SyncedSeriesWorldElementResult[] = SeriesWorldRepo.fetchSyncedSeriesWorldElements(userId, lang); - const allLocations: SyncedSeriesLocationResult[] = SeriesLocationRepo.fetchSyncedSeriesLocations(userId, lang); - const allLocationElements: SyncedSeriesLocationElementResult[] = SeriesLocationRepo.fetchSyncedSeriesLocationElements(userId, lang); - const allLocationSubElements: SyncedSeriesLocationSubElementResult[] = SeriesLocationRepo.fetchSyncedSeriesLocationSubElements(userId, lang); - const allSpells: SyncedSeriesSpellResult[] = SeriesSpellRepo.fetchSyncedSeriesSpells(userId, lang); - const allSpellTags: SyncedSeriesSpellTagResult[] = SeriesSpellRepo.fetchSyncedSeriesSpellTags(userId, lang); - - return allSeries.map((series: SyncedSeriesResult): SyncedSeries => { - const seriesId: string = series.series_id; - - // Map series books - const books: SyncedSeriesBook[] = allSeriesBooks - .filter((sb: SyncedSeriesBookResult): boolean => sb.series_id === seriesId) - .map((sb: SyncedSeriesBookResult): SyncedSeriesBook => ({ - bookId: sb.book_id, - order: sb.book_order, - lastUpdate: sb.last_update - })); - - // Map characters with attributes - const characters: SyncedSeriesCharacter[] = allCharacters - .filter((c: SyncedSeriesCharacterResult): boolean => c.series_id === seriesId) - .map((c: SyncedSeriesCharacterResult): SyncedSeriesCharacter => ({ - id: c.character_id, - name: System.decryptDataWithUserKey(c.first_name, userEncryptionKey), - lastUpdate: c.last_update, - attributes: allCharacterAttributes - .filter((a: SyncedSeriesCharacterAttributeResult): boolean => a.character_id === c.character_id) - .map((a: SyncedSeriesCharacterAttributeResult): SyncedSeriesCharacterAttribute => ({ - id: a.attr_id, - name: System.decryptDataWithUserKey(a.attribute_name, userEncryptionKey), - lastUpdate: a.last_update - })) - })); - - // Map worlds with elements - const worlds: SyncedSeriesWorld[] = allWorlds - .filter((w: SyncedSeriesWorldResult): boolean => w.series_id === seriesId) - .map((w: SyncedSeriesWorldResult): SyncedSeriesWorld => ({ - id: w.world_id, - name: System.decryptDataWithUserKey(w.name, userEncryptionKey), - lastUpdate: w.last_update, - elements: allWorldElements - .filter((e: SyncedSeriesWorldElementResult): boolean => e.world_id === w.world_id) - .map((e: SyncedSeriesWorldElementResult): SyncedSeriesWorldElement => ({ - id: e.element_id, - name: System.decryptDataWithUserKey(e.name, userEncryptionKey), - lastUpdate: e.last_update - })) - })); - - // Map locations with elements and sub-elements - const locations: SyncedSeriesLocation[] = allLocations - .filter((l: SyncedSeriesLocationResult): boolean => l.series_id === seriesId) - .map((l: SyncedSeriesLocationResult): SyncedSeriesLocation => ({ - id: l.loc_id, - name: System.decryptDataWithUserKey(l.loc_name, userEncryptionKey), - lastUpdate: l.last_update, - elements: allLocationElements - .filter((e: SyncedSeriesLocationElementResult): boolean => e.location_id === l.loc_id) - .map((e: SyncedSeriesLocationElementResult): SyncedSeriesLocationElement => ({ - id: e.element_id, - name: System.decryptDataWithUserKey(e.element_name, userEncryptionKey), - lastUpdate: e.last_update, - subElements: allLocationSubElements - .filter((se: SyncedSeriesLocationSubElementResult): boolean => se.element_id === e.element_id) - .map((se: SyncedSeriesLocationSubElementResult): SyncedSeriesLocationSubElement => ({ - id: se.sub_element_id, - name: System.decryptDataWithUserKey(se.sub_elem_name, userEncryptionKey), - lastUpdate: se.last_update - })) - })) - })); - - // Map spells - const spells: SyncedSeriesSpell[] = allSpells - .filter((s: SyncedSeriesSpellResult): boolean => s.series_id === seriesId) - .map((s: SyncedSeriesSpellResult): SyncedSeriesSpell => ({ - id: s.spell_id, - name: System.decryptDataWithUserKey(s.name, userEncryptionKey), - lastUpdate: s.last_update - })); - - // Map spell tags - const spellTags: SyncedSeriesSpellTag[] = allSpellTags - .filter((t: SyncedSeriesSpellTagResult): boolean => t.series_id === seriesId) - .map((t: SyncedSeriesSpellTagResult): SyncedSeriesSpellTag => ({ - id: t.tag_id, - name: System.decryptDataWithUserKey(t.name, userEncryptionKey), - lastUpdate: t.last_update - })); - - return { - id: seriesId, - name: System.decryptDataWithUserKey(series.name, userEncryptionKey), - description: series.description - ? System.decryptDataWithUserKey(series.description, userEncryptionKey) - : null, - lastUpdate: series.last_update, - books, - characters, - worlds, - locations, - spells, - spellTags - }; - }); - } -} diff --git a/electron/database/models/Upload.ts b/electron/database/models/Upload.ts deleted file mode 100644 index e250077..0000000 --- a/electron/database/models/Upload.ts +++ /dev/null @@ -1,301 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import { CompleteBook } from "./Book.js"; -import BookRepo, { EritBooksTable, BookToolsTable } from "../repositories/book.repository.js"; -import ActRepository, { BookActSummariesTable } from "../repositories/act.repository.js"; -import GuidelineRepo, { BookAIGuideLineTable, BookGuideLineTable } from "../repositories/guideline.repository.js"; -import ChapterRepo, { - BookChapterInfosTable, - BookChaptersTable -} from "../repositories/chapter.repository.js"; -import CharacterRepo, { - BookCharactersAttributesTable, - BookCharactersTable -} from "../repositories/character.repository.js"; -import IncidentRepository, { BookIncidentsTable } from "../repositories/incident.repository.js"; -import IssueRepository, { BookIssuesTable } from "../repositories/issue.repository.js"; -import LocationRepo, { - BookLocationTable, - LocationElementTable, - LocationSubElementTable -} from "../repositories/location.repository.js"; -import PlotPointRepository, { BookPlotPointsTable } from "../repositories/plotpoint.repository.js"; -import WorldRepository, { - BookWorldElementsTable, - 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 { - /** - * Prepares a complete book with all related data for synchronization upload. - * Fetches all book-related tables from the database, decrypts encrypted fields - * using the user's encryption key, and returns a complete book object ready for sync. - * - * @param userId - The unique identifier of the user who owns the book - * @param bookId - The unique identifier of the book to upload - * @param lang - The language code for localization ("fr" or "en") - * @returns A promise that resolves to a CompleteBook object containing all decrypted book data - */ - static async uploadBookForSync(userId: string, bookId: string, lang: "fr" | "en"): Promise { - const userEncryptionKey: string = getUserEncryptionKey(userId); - - const [ - encryptedBooks, - encryptedActSummaries, - encryptedAIGuidelines, - encryptedChapters, - encryptedCharacters, - encryptedGuidelines, - encryptedIncidents, - encryptedIssues, - encryptedLocations, - encryptedPlotPoints, - encryptedWorlds, - bookToolsData, - encryptedSpells, - encryptedSpellTags - ]: [ - EritBooksTable[], - BookActSummariesTable[], - BookAIGuideLineTable[], - BookChaptersTable[], - BookCharactersTable[], - BookGuideLineTable[], - BookIncidentsTable[], - BookIssuesTable[], - BookLocationTable[], - BookPlotPointsTable[], - BookWorldTable[], - BookToolsTable | null, - BookSpellsTable[], - BookSpellTagsTable[] - ] = await Promise.all([ - BookRepo.fetchEritBooksTable(userId, bookId, lang), - ActRepository.fetchBookActSummaries(userId, bookId, lang), - GuidelineRepo.fetchBookAIGuideLine(userId, bookId, lang), - ChapterRepo.fetchBookChapters(userId, bookId, lang), - CharacterRepo.fetchBookCharacters(userId, bookId, lang), - GuidelineRepo.fetchBookGuideLineTable(userId, bookId, lang), - IncidentRepository.fetchBookIncidents(userId, bookId, lang), - IssueRepository.fetchBookIssues(userId, bookId, lang), - LocationRepo.fetchBookLocations(userId, bookId, lang), - PlotPointRepository.fetchBookPlotPoints(userId, bookId, lang), - WorldRepository.fetchBookWorlds(userId, bookId, lang), - BookRepo.fetchBookTools(userId, bookId, lang), - SpellRepo.fetchBookSpellsTable(userId, bookId, lang), - SpellTagRepo.fetchBookSpellTagsTable(userId, bookId, lang) - ]); - - const [ - nestedChapterContents, - nestedChapterInfos, - nestedCharacterAttributes, - nestedWorldElements, - nestedLocationElements - ]: [ - BookChapterContentTable[][], - BookChapterInfosTable[][], - BookCharactersAttributesTable[][], - BookWorldElementsTable[][], - LocationElementTable[][] - ] = await Promise.all([ - Promise.all(encryptedChapters.map((chapter: BookChaptersTable): Promise => - ChapterContentRepository.fetchBookChapterContents(userId, chapter.chapter_id, lang))), - Promise.all(encryptedChapters.map((chapter: BookChaptersTable): Promise => - ChapterRepo.fetchBookChapterInfos(userId, chapter.chapter_id, lang))), - Promise.all(encryptedCharacters.map((character: BookCharactersTable): Promise => - CharacterRepo.fetchBookCharactersAttributes(userId, character.character_id, lang))), - Promise.all(encryptedWorlds.map((world: BookWorldTable): Promise => - WorldRepository.fetchBookWorldElements(userId, world.world_id, lang))), - Promise.all(encryptedLocations.map((location: BookLocationTable): Promise => - LocationRepo.fetchLocationElements(userId, location.loc_id, lang))) - ]); - - const encryptedChapterContents: BookChapterContentTable[] = nestedChapterContents.flat(); - const encryptedChapterInfos: BookChapterInfosTable[] = nestedChapterInfos.flat(); - const encryptedCharacterAttributes: BookCharactersAttributesTable[] = nestedCharacterAttributes.flat(); - const encryptedWorldElements: BookWorldElementsTable[] = nestedWorldElements.flat(); - const encryptedLocationElements: LocationElementTable[] = nestedLocationElements.flat(); - - const nestedLocationSubElements: LocationSubElementTable[][] = await Promise.all( - encryptedLocationElements.map((element: LocationElementTable): Promise => - LocationRepo.fetchLocationSubElements(userId, element.element_id, lang)) - ); - const encryptedLocationSubElements: LocationSubElementTable[] = nestedLocationSubElements.flat(); - - const eritBooks: EritBooksTable[] = encryptedBooks.map((book: EritBooksTable): EritBooksTable => ({ - ...book, - title: System.decryptDataWithUserKey(book.title, userEncryptionKey), - sub_title: book.sub_title ? System.decryptDataWithUserKey(book.sub_title, userEncryptionKey) : null, - summary: book.summary ? System.decryptDataWithUserKey(book.summary, userEncryptionKey) : null, - cover_image: book.cover_image ? System.decryptDataWithUserKey(book.cover_image, userEncryptionKey) : null - })); - - const actSummaries: BookActSummariesTable[] = encryptedActSummaries.map((actSummary: BookActSummariesTable): BookActSummariesTable => ({ - ...actSummary, - summary: actSummary.summary ? System.decryptDataWithUserKey(actSummary.summary, userEncryptionKey) : null - })); - - const aiGuideLine: BookAIGuideLineTable[] = encryptedAIGuidelines.map((guideLine: BookAIGuideLineTable): BookAIGuideLineTable => ({ - ...guideLine, - global_resume: guideLine.global_resume ? System.decryptDataWithUserKey(guideLine.global_resume, userEncryptionKey) : null, - themes: guideLine.themes ? System.decryptDataWithUserKey(guideLine.themes, userEncryptionKey) : null, - tone: guideLine.tone ? System.decryptDataWithUserKey(guideLine.tone, userEncryptionKey) : null, - atmosphere: guideLine.atmosphere ? System.decryptDataWithUserKey(guideLine.atmosphere, userEncryptionKey) : null, - current_resume: guideLine.current_resume ? System.decryptDataWithUserKey(guideLine.current_resume, userEncryptionKey) : null - })); - - const chapters: BookChaptersTable[] = encryptedChapters.map((chapter: BookChaptersTable): BookChaptersTable => ({ - ...chapter, - title: System.decryptDataWithUserKey(chapter.title, userEncryptionKey) - })); - - const chapterContents: BookChapterContentTable[] = encryptedChapterContents.map((chapterContent: BookChapterContentTable): BookChapterContentTable => ({ - ...chapterContent, - content: chapterContent.content ? JSON.parse(System.decryptDataWithUserKey(chapterContent.content, userEncryptionKey)) : null - })); - - const chapterInfos: BookChapterInfosTable[] = encryptedChapterInfos.map((chapterInfo: BookChapterInfosTable): BookChapterInfosTable => ({ - ...chapterInfo, - summary: chapterInfo.summary ? System.decryptDataWithUserKey(chapterInfo.summary, userEncryptionKey) : null, - goal: chapterInfo.goal ? System.decryptDataWithUserKey(chapterInfo.goal, userEncryptionKey) : null - })); - - const characters: BookCharactersTable[] = encryptedCharacters.map((character: BookCharactersTable): BookCharactersTable => ({ - ...character, - first_name: System.decryptDataWithUserKey(character.first_name, userEncryptionKey), - last_name: character.last_name ? System.decryptDataWithUserKey(character.last_name, userEncryptionKey) : null, - nickname: character.nickname ? System.decryptDataWithUserKey(character.nickname, userEncryptionKey) : null, - age: character.age ? System.decryptDataWithUserKey(character.age, userEncryptionKey) : null, - gender: character.gender ? System.decryptDataWithUserKey(character.gender, userEncryptionKey) : null, - species: character.species ? System.decryptDataWithUserKey(character.species, userEncryptionKey) : null, - nationality: character.nationality ? System.decryptDataWithUserKey(character.nationality, userEncryptionKey) : null, - status: character.status ? System.decryptDataWithUserKey(character.status, userEncryptionKey) : null, - category: System.decryptDataWithUserKey(character.category, userEncryptionKey), - title: character.title ? System.decryptDataWithUserKey(character.title, userEncryptionKey) : null, - role: character.role ? System.decryptDataWithUserKey(character.role, userEncryptionKey) : null, - biography: character.biography ? System.decryptDataWithUserKey(character.biography, userEncryptionKey) : null, - history: character.history ? System.decryptDataWithUserKey(character.history, userEncryptionKey) : null, - speech_pattern: character.speech_pattern ? System.decryptDataWithUserKey(character.speech_pattern, userEncryptionKey) : null, - catchphrase: character.catchphrase ? System.decryptDataWithUserKey(character.catchphrase, userEncryptionKey) : null, - residence: character.residence ? System.decryptDataWithUserKey(character.residence, userEncryptionKey) : null, - notes: character.notes ? System.decryptDataWithUserKey(character.notes, userEncryptionKey) : null, - color: character.color ? System.decryptDataWithUserKey(character.color, userEncryptionKey) : null - })); - - const characterAttributes: BookCharactersAttributesTable[] = encryptedCharacterAttributes.map((attribute: BookCharactersAttributesTable): BookCharactersAttributesTable => ({ - ...attribute, - attribute_name: System.decryptDataWithUserKey(attribute.attribute_name, userEncryptionKey), - attribute_value: System.decryptDataWithUserKey(attribute.attribute_value, userEncryptionKey) - })); - - const guideLine: BookGuideLineTable[] = encryptedGuidelines.map((guide: BookGuideLineTable): BookGuideLineTable => ({ - ...guide, - tone: guide.tone ? System.decryptDataWithUserKey(guide.tone, userEncryptionKey) : null, - atmosphere: guide.atmosphere ? System.decryptDataWithUserKey(guide.atmosphere, userEncryptionKey) : null, - writing_style: guide.writing_style ? System.decryptDataWithUserKey(guide.writing_style, userEncryptionKey) : null, - themes: guide.themes ? System.decryptDataWithUserKey(guide.themes, userEncryptionKey) : null, - symbolism: guide.symbolism ? System.decryptDataWithUserKey(guide.symbolism, userEncryptionKey) : null, - motifs: guide.motifs ? System.decryptDataWithUserKey(guide.motifs, userEncryptionKey) : null, - narrative_voice: guide.narrative_voice ? System.decryptDataWithUserKey(guide.narrative_voice, userEncryptionKey) : null, - pacing: guide.pacing ? System.decryptDataWithUserKey(guide.pacing, userEncryptionKey) : null, - intended_audience: guide.intended_audience ? System.decryptDataWithUserKey(guide.intended_audience, userEncryptionKey) : null, - key_messages: guide.key_messages ? System.decryptDataWithUserKey(guide.key_messages, userEncryptionKey) : null - })); - - const incidents: BookIncidentsTable[] = encryptedIncidents.map((incident: BookIncidentsTable): BookIncidentsTable => ({ - ...incident, - title: System.decryptDataWithUserKey(incident.title, userEncryptionKey), - summary: incident.summary ? System.decryptDataWithUserKey(incident.summary, userEncryptionKey) : null - })); - - const issues: BookIssuesTable[] = encryptedIssues.map((issue: BookIssuesTable): BookIssuesTable => ({ - ...issue, - name: System.decryptDataWithUserKey(issue.name, userEncryptionKey) - })); - - const locations: BookLocationTable[] = encryptedLocations.map((location: BookLocationTable): BookLocationTable => ({ - ...location, - loc_name: System.decryptDataWithUserKey(location.loc_name, userEncryptionKey) - })); - - const plotPoints: BookPlotPointsTable[] = encryptedPlotPoints.map((plotPoint: BookPlotPointsTable): BookPlotPointsTable => ({ - ...plotPoint, - title: System.decryptDataWithUserKey(plotPoint.title, userEncryptionKey), - summary: plotPoint.summary ? System.decryptDataWithUserKey(plotPoint.summary, userEncryptionKey) : null - })); - - const worlds: BookWorldTable[] = encryptedWorlds.map((world: BookWorldTable): BookWorldTable => ({ - ...world, - name: System.decryptDataWithUserKey(world.name, userEncryptionKey), - history: world.history ? System.decryptDataWithUserKey(world.history, userEncryptionKey) : null, - politics: world.politics ? System.decryptDataWithUserKey(world.politics, userEncryptionKey) : null, - economy: world.economy ? System.decryptDataWithUserKey(world.economy, userEncryptionKey) : null, - religion: world.religion ? System.decryptDataWithUserKey(world.religion, userEncryptionKey) : null, - languages: world.languages ? System.decryptDataWithUserKey(world.languages, userEncryptionKey) : null - })); - - const worldElements: BookWorldElementsTable[] = encryptedWorldElements.map((worldElement: BookWorldElementsTable): BookWorldElementsTable => ({ - ...worldElement, - name: System.decryptDataWithUserKey(worldElement.name, userEncryptionKey), - description: worldElement.description ? System.decryptDataWithUserKey(worldElement.description, userEncryptionKey) : null - })); - - const locationElements: LocationElementTable[] = encryptedLocationElements.map((locationElement: LocationElementTable): LocationElementTable => ({ - ...locationElement, - element_name: System.decryptDataWithUserKey(locationElement.element_name, userEncryptionKey), - element_description: locationElement.element_description ? System.decryptDataWithUserKey(locationElement.element_description, userEncryptionKey) : null - })); - - const locationSubElements: LocationSubElementTable[] = encryptedLocationSubElements.map((locationSubElement: LocationSubElementTable): LocationSubElementTable => ({ - ...locationSubElement, - sub_elem_name: System.decryptDataWithUserKey(locationSubElement.sub_elem_name, userEncryptionKey), - sub_elem_description: locationSubElement.sub_elem_description ? System.decryptDataWithUserKey(locationSubElement.sub_elem_description, userEncryptionKey) : null - })); - - const bookTools: BookToolsTable[] = bookToolsData ? [bookToolsData] : []; - - const spells: BookSpellsTable[] = encryptedSpells.map((spell: BookSpellsTable): BookSpellsTable => ({ - ...spell, - name: System.decryptDataWithUserKey(spell.name, userEncryptionKey), - description: spell.description ? System.decryptDataWithUserKey(spell.description, userEncryptionKey) : null, - appearance: spell.appearance ? System.decryptDataWithUserKey(spell.appearance, userEncryptionKey) : null, - tags: spell.tags ? System.decryptDataWithUserKey(spell.tags, userEncryptionKey) : null, - 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, - aiGuideLine, - chapters, - chapterContents, - chapterInfos, - characters, - characterAttributes, - guideLine, - incidents, - issues, - locations, - plotPoints, - worlds, - worldElements, - locationElements, - locationSubElements, - bookTools, - spells, - spellTags - }; - } -} diff --git a/electron/database/models/User.ts b/electron/database/models/User.ts deleted file mode 100644 index 13f425e..0000000 --- a/electron/database/models/User.ts +++ /dev/null @@ -1,302 +0,0 @@ -import UserRepo, {UserAccountQuery, UserInfosQueryResponse} from "../repositories/user.repository.js"; -import System from "../System.js"; -import Book, {BookProps} from "./Book.js"; -import {getUserEncryptionKey} from "../keyManager.js"; - -/** - * Represents a user account with basic profile information. - */ -interface UserAccount { - firstName: string; - lastName: string; - username: string; - authorName: string; - email: string; -} - -/** - * Represents the guide tour completion status for various features. - */ -export interface GuideTour { - [key: string]: boolean; -} - -/** - * Summary information for a book associated with a user. - */ -interface BookSummary { - bookId: string; - title: string; - subTitle?: string; -} - -/** - * Complete user information response including profile data and associated books. - */ -export interface UserInfoResponse { - id: string; - name: string; - lastName: string; - username: string; - email: string; - accountVerified: boolean; - authorName: string; - groupId: number; - termsAccepted: boolean; - guideTour: GuideTour[]; -} - -/** - * Represents a user entity with encrypted personal information storage. - * Handles user data retrieval, creation, and updates with AES-256-CBC encryption. - */ -export default class User { - - private readonly id: string; - private firstName: string; - private lastName: string; - private username: string; - private email: string; - private accountVerified: boolean; - private authorName: string; - private groupId: number; - private termsAccepted: boolean; - - /** - * Creates a new User instance with the specified identifier. - * @param id - The unique identifier for the user - */ - constructor(id: string) { - this.id = id; - this.firstName = ''; - this.lastName = ''; - this.username = ''; - this.email = ''; - this.accountVerified = false; - this.authorName = ''; - this.groupId = 0; - this.termsAccepted = false; - } - - /** - * Fetches and decrypts the user's information from the database. - * Populates all instance properties with the decrypted values. - * @returns A promise that resolves when user information has been loaded - */ - public async getUserInfos(): Promise { - const userInfosData: UserInfosQueryResponse = UserRepo.fetchUserInfos(this.id); - const userEncryptionKey: string = getUserEncryptionKey(this.id); - this.firstName = System.decryptDataWithUserKey(userInfosData.first_name, userEncryptionKey); - this.lastName = System.decryptDataWithUserKey(userInfosData.last_name, userEncryptionKey); - this.username = System.decryptDataWithUserKey(userInfosData.username, userEncryptionKey); - this.email = System.decryptDataWithUserKey(userInfosData.email, userEncryptionKey); - this.accountVerified = userInfosData.account_verified === 1; - this.authorName = userInfosData.author_name ? System.decryptDataWithUserKey(userInfosData.author_name, userEncryptionKey) : ''; - this.groupId = userInfosData.user_group ? userInfosData.user_group : 0; - this.termsAccepted = userInfosData.term_accepted === 1; - } - - /** - * Retrieves complete user information including associated books. - * @param userId - The unique identifier of the user to fetch - * @returns A promise resolving to the complete user information response - */ - public static async returnUserInfos(userId: string): Promise { - const user: User = new User(userId); - await user.getUserInfos(); - const guideTourStatus: GuideTour[] = []; - return { - id: user.getId(), - name: user.getFirstName(), - lastName: user.getLastName(), - username: user.getUsername(), - email: user.getEmail(), - accountVerified: user.isAccountVerified(), - authorName: user.getAuthorName(), - groupId: user.getGroupId(), - termsAccepted: user.isTermsAccepted(), - guideTour: guideTourStatus, - }; - } - - /** - * Creates a new user in the database with encrypted personal information. - * @param userId - The unique identifier for the new user - * @param firstName - The user's first name (will be encrypted) - * @param lastName - The user's last name (will be encrypted) - * @param username - The user's username (will be encrypted and hashed) - * @param email - The user's email address (will be encrypted and hashed) - * @param notEncryptPassword - The user's password in plain text (unused in current implementation) - * @param lang - The preferred language for the user ('fr' or 'en'), defaults to 'fr' - * @returns A promise resolving to the created user's identifier - */ - public static async addUser( - userId: string, - firstName: string, - lastName: string, - username: string, - email: string, - notEncryptPassword: string, - lang: 'fr' | 'en' = 'fr' - ): Promise { - const userEncryptionKey: string = getUserEncryptionKey(userId); - const encryptedFirstName: string = System.encryptDataWithUserKey(firstName, userEncryptionKey); - const encryptedLastName: string = System.encryptDataWithUserKey(lastName, userEncryptionKey); - const encryptedUsername: string = System.encryptDataWithUserKey(username, userEncryptionKey); - const encryptedEmail: string = System.encryptDataWithUserKey(email, userEncryptionKey); - const hashedEmail: string = System.hashElement(email); - const hashedUsername: string = System.hashElement(username); - return UserRepo.insertUser( - userId, - encryptedFirstName, - encryptedLastName, - encryptedUsername, - hashedUsername, - encryptedEmail, - hashedEmail, - lang - ); - } - - /** - * Updates an existing user's profile information in the database. - * @param userKey - The encryption key for the user's data - * @param userId - The unique identifier of the user to update - * @param firstName - The updated first name (will be encrypted) - * @param lastName - The updated last name (will be encrypted) - * @param username - The updated username (will be encrypted and hashed) - * @param email - The updated email address (will be encrypted and hashed) - * @param authorName - The optional author/pen name (will be encrypted and hashed if provided) - * @param lang - The preferred language for the user ('fr' or 'en'), defaults to 'fr' - * @returns A promise resolving to true if the update was successful - */ - public static async updateUserInfos( - userKey: string, - userId: string, - firstName: string, - lastName: string, - username: string, - email: string, - authorName?: string, - lang: 'fr' | 'en' = 'fr' - ): Promise { - const encryptedFirstName: string = System.encryptDataWithUserKey(firstName, userKey); - const encryptedLastName: string = System.encryptDataWithUserKey(lastName, userKey); - const encryptedUsername: string = System.encryptDataWithUserKey(username, userKey); - const encryptedEmail: string = System.encryptDataWithUserKey(email, userKey); - const hashedEmail: string = System.hashElement(email); - const hashedUsername: string = System.hashElement(username); - let encryptedAuthorName: string = ''; - let hashedAuthorName: string = ''; - if (authorName) { - encryptedAuthorName = System.encryptDataWithUserKey(authorName, userKey); - hashedAuthorName = System.hashElement(authorName); - } - return UserRepo.updateUserInfos( - userId, - encryptedFirstName, - encryptedLastName, - encryptedUsername, - hashedUsername, - encryptedEmail, - hashedEmail, - hashedAuthorName, - encryptedAuthorName, - lang - ); - } - - /** - * Retrieves and decrypts the user's account information from the database. - * @param userId - The unique identifier of the user - * @returns A promise resolving to the decrypted user account information - */ - public static async getUserAccountInformation(userId: string): Promise { - const accountData: UserAccountQuery = UserRepo.fetchAccountInformation(userId); - const userEncryptionKey: string = getUserEncryptionKey(userId); - const decryptedFirstName: string = accountData.first_name ? System.decryptDataWithUserKey(accountData.first_name, userEncryptionKey) : ''; - const decryptedLastName: string = accountData.last_name ? System.decryptDataWithUserKey(accountData.last_name, userEncryptionKey) : ''; - const decryptedUsername: string = accountData.username ? System.decryptDataWithUserKey(accountData.username, userEncryptionKey) : ''; - const decryptedAuthorName: string = accountData.author_name ? System.decryptDataWithUserKey(accountData.author_name, userEncryptionKey) : ''; - const decryptedEmail: string = accountData.email ? System.decryptDataWithUserKey(accountData.email, userEncryptionKey) : ''; - return { - firstName: decryptedFirstName, - lastName: decryptedLastName, - username: decryptedUsername, - authorName: decryptedAuthorName, - email: decryptedEmail - }; - } - - /** - * Gets the unique identifier of the user. - * @returns The user's unique identifier - */ - public getId(): string { - return this.id; - } - - /** - * Gets the user's first name. - * @returns The user's first name - */ - public getFirstName(): string { - return this.firstName; - } - - /** - * Gets the user's last name. - * @returns The user's last name - */ - public getLastName(): string { - return this.lastName; - } - - /** - * Gets the user's username. - * @returns The user's username - */ - public getUsername(): string { - return this.username; - } - - /** - * Gets the user's email address. - * @returns The user's email address - */ - public getEmail(): string { - return this.email; - } - - /** - * Checks if the user's account has been verified. - * @returns True if the account is verified, false otherwise - */ - public isAccountVerified(): boolean { - return this.accountVerified; - } - - /** - * Checks if the user has accepted the terms of service. - * @returns True if the terms have been accepted, false otherwise - */ - public isTermsAccepted(): boolean { - return this.termsAccepted; - } - - /** - * Gets the user's group identifier. - * @returns The user's group identifier - */ - public getGroupId(): number { - return this.groupId; - } - - /** - * Gets the user's author/pen name. - * @returns The user's author name - */ - public getAuthorName(): string { - return this.authorName; - } -} diff --git a/electron/database/models/World.ts b/electron/database/models/World.ts deleted file mode 100644 index 41463df..0000000 --- a/electron/database/models/World.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { getUserEncryptionKey } from "../keyManager.js"; -import System from "../System.js"; -import WorldRepository, { WorldElementValue, WorldQuery } from "../repositories/world.repository.js"; -import BookRepo, {BookToolsTable} from "../repositories/book.repository.js"; -import RemovedItem from "./RemovedItem.js"; - -export interface SyncedWorld { - id: string; - name: string; - lastUpdate: number; - elements: SyncedWorldElement[]; -} - -export interface SyncedWorldElement { - id: string; - name: string; - lastUpdate: number; -} - -export interface WorldElement { - id: string; - name: string; - description: string; - type?: number; -} - -export interface WorldProps { - id: string; - name: string; - history: string; - politics: string; - economy: string; - religion: string; - languages: string; - laws: WorldElement[]; - biomes: WorldElement[]; - issues: WorldElement[]; - customs: WorldElement[]; - kingdoms: WorldElement[]; - climate: WorldElement[]; - resources: WorldElement[]; - wildlife: WorldElement[]; - arts: WorldElement[]; - ethnicGroups: WorldElement[]; - socialClasses: WorldElement[]; - importantCharacters: WorldElement[]; - seriesWorldId?: string | null; -} - -export interface WorldListResponse { - worlds: WorldProps[]; - enabled: boolean; -} - -/** - * Mapping of element type keys to their corresponding numeric type identifiers. - */ -const ELEMENT_TYPE_MAP: Record = { - laws: 1, - biomes: 2, - issues: 3, - customs: 4, - kingdoms: 5, - climate: 6, - resources: 7, - wildlife: 8, - arts: 9, - ethnicGroups: 10, - socialClasses: 11, - importantCharacters: 12 -}; - -/** - * Mapping of numeric type identifiers to their corresponding WorldProps keys. - */ -const ELEMENT_TYPE_KEYS: Record = { - 1: 'laws', - 2: 'biomes', - 3: 'issues', - 4: 'customs', - 5: 'kingdoms', - 6: 'climate', - 7: 'resources', - 8: 'wildlife', - 9: 'arts', - 10: 'ethnicGroups', - 11: 'socialClasses', - 12: 'importantCharacters' -}; - -export default class World { - /** - * Creates a new world for a book. - * @param userId - The unique identifier of the user creating the world - * @param bookId - The unique identifier of the book to associate the world with - * @param worldName - The name of the new world - * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr' - * @param existingWorldId - Optional existing world ID for syncing purposes - * @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, seriesWorldId: string | null = null): string { - const userEncryptionKey: string = getUserEncryptionKey(userId); - const hashedWorldName: string = System.hashElement(worldName); - if (!existingWorldId && WorldRepository.checkWorldExist(userId, bookId, hashedWorldName, lang)) { - throw new Error(lang === "fr" ? `Tu as déjà un monde ${worldName}.` : `You already have a world named ${worldName}.`); - } - const encryptedWorldName: string = System.encryptDataWithUserKey(worldName, userEncryptionKey); - const worldId: string = existingWorldId || System.createUniqueId(); - return WorldRepository.insertNewWorld(worldId, userId, bookId, encryptedWorldName, hashedWorldName, lang, seriesWorldId); - } - - /** - * Retrieves all worlds and their elements 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'), defaults to 'fr' - * @returns WorldListResponse containing an array of WorldProps and enabled flag - */ - public static getWorlds(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): WorldListResponse { - const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang); - const enabled: boolean = bookTools ? bookTools.worlds_enabled === 1 : false; - - const worldQueryResults: WorldQuery[] = WorldRepository.fetchWorlds(userId, bookId, lang); - const userEncryptionKey: string = getUserEncryptionKey(userId); - const worlds: WorldProps[] = []; - - for (const queryRow of worldQueryResults) { - const existingWorld: WorldProps | undefined = worlds.find((world: WorldProps) => world.id === queryRow.world_id); - - if (!existingWorld) { - const newWorld: WorldProps = { - id: queryRow.world_id, - name: System.decryptDataWithUserKey(queryRow.world_name, userEncryptionKey), - history: queryRow.history ? System.decryptDataWithUserKey(queryRow.history, userEncryptionKey) : '', - politics: queryRow.politics ? System.decryptDataWithUserKey(queryRow.politics, userEncryptionKey) : '', - economy: queryRow.economy ? System.decryptDataWithUserKey(queryRow.economy, userEncryptionKey) : '', - religion: queryRow.religion ? System.decryptDataWithUserKey(queryRow.religion, userEncryptionKey) : '', - languages: queryRow.languages ? System.decryptDataWithUserKey(queryRow.languages, userEncryptionKey) : '', - laws: [], - biomes: [], - issues: [], - customs: [], - kingdoms: [], - climate: [], - resources: [], - wildlife: [], - arts: [], - ethnicGroups: [], - socialClasses: [], - importantCharacters: [], - seriesWorldId: queryRow.series_world_id || null, - }; - - worlds.push(newWorld); - - if (queryRow.element_type) { - const worldElement: WorldElement = { - id: queryRow.element_id as string, - name: queryRow.element_name ? System.decryptDataWithUserKey(queryRow.element_name, userEncryptionKey) : '', - description: queryRow.element_description ? System.decryptDataWithUserKey(queryRow.element_description, userEncryptionKey) : '' - }; - - const elementKey: keyof WorldProps | undefined = ELEMENT_TYPE_KEYS[queryRow.element_type]; - if (elementKey) { - (worlds[worlds.length - 1][elementKey] as WorldElement[]).push(worldElement); - } - } - } else { - const worldElement: WorldElement = { - id: queryRow.element_id as string, - name: queryRow.element_name ? System.decryptDataWithUserKey(queryRow.element_name, userEncryptionKey) : '', - description: queryRow.element_description ? System.decryptDataWithUserKey(queryRow.element_description, userEncryptionKey) : '' - }; - - const elementKey: keyof WorldProps | undefined = ELEMENT_TYPE_KEYS[queryRow.element_type as number]; - if (elementKey) { - (existingWorld[elementKey] as WorldElement[]).push(worldElement); - } - } - } - return { worlds, enabled }; - } - - /** - * Updates a world's properties and all its elements. - * @param userId - The unique identifier of the user - * @param world - The WorldProps object containing updated world data - * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr' - * @returns True if the update was successful, false otherwise - */ - public static updateWorld(userId: string, world: WorldProps, lang: 'fr' | 'en' = 'fr'): boolean { - const userEncryptionKey: string = getUserEncryptionKey(userId); - const encryptedName: string = world.name ? System.encryptDataWithUserKey(world.name, userEncryptionKey) : ''; - const encryptedHistory: string = world.history ? System.encryptDataWithUserKey(world.history, userEncryptionKey) : ''; - const encryptedPolitics: string = world.politics ? System.encryptDataWithUserKey(world.politics, userEncryptionKey) : ''; - const encryptedEconomy: string = world.economy ? System.encryptDataWithUserKey(world.economy, userEncryptionKey) : ''; - const encryptedReligion: string = world.religion ? System.encryptDataWithUserKey(world.religion, userEncryptionKey) : ''; - const encryptedLanguages: string = world.languages ? System.encryptDataWithUserKey(world.languages, userEncryptionKey) : ''; - - let elementsToUpdate: WorldElementValue[] = []; - const elementCategories: { key: keyof WorldProps; elements: WorldElement[] }[] = [ - { key: 'laws', elements: world.laws }, - { key: 'biomes', elements: world.biomes }, - { key: 'issues', elements: world.issues }, - { key: 'customs', elements: world.customs }, - { key: 'kingdoms', elements: world.kingdoms }, - { key: 'climate', elements: world.climate }, - { key: 'resources', elements: world.resources }, - { key: 'wildlife', elements: world.wildlife }, - { key: 'arts', elements: world.arts }, - { key: 'ethnicGroups', elements: world.ethnicGroups }, - { key: 'socialClasses', elements: world.socialClasses }, - { key: 'importantCharacters', elements: world.importantCharacters } - ]; - - elementCategories.forEach(({ key, elements: categoryElements }) => { - elementsToUpdate = elementsToUpdate.concat(categoryElements.map((worldElement: WorldElement) => { - const encryptedElementName: string = System.encryptDataWithUserKey(worldElement.name, userEncryptionKey); - const hashedElementName: string = System.hashElement(worldElement.name); - const encryptedDescription: string = worldElement.description ? System.encryptDataWithUserKey(worldElement.description, userEncryptionKey) : ''; - const elementTypeId: number = World.getElementTypes(key); - - return { - id: worldElement.id, - name: encryptedElementName, - hashedName: hashedElementName, - description: encryptedDescription, - type: elementTypeId - }; - })); - }); - - 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); - } - - /** - * Adds a new element to an existing world. - * @param userId - The unique identifier of the user - * @param worldId - The unique identifier of the world to add the element to - * @param elementName - The name of the new element - * @param elementType - The type of element (e.g., 'laws', 'biomes', 'customs') - * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr' - * @param existingElementId - Optional existing element ID for syncing purposes - * @returns The unique identifier of the newly created element - * @throws Error if an element with the same name already exists in this world - */ - public static addNewElementToWorld(userId: string, worldId: string, elementName: string, elementType: string, lang: 'fr' | 'en' = 'fr', existingElementId?: string): string { - const userEncryptionKey: string = getUserEncryptionKey(userId); - const hashedElementName: string = System.hashElement(elementName); - if (!existingElementId && WorldRepository.checkElementExist(worldId, hashedElementName, lang)) { - throw new Error(lang === "fr" ? `Vous avez déjà un élément avec ce nom ${elementName}.` : `You already have an element named ${elementName}.`); - } - const elementTypeId: number = World.getElementTypes(elementType); - const encryptedElementName: string = System.encryptDataWithUserKey(elementName, userEncryptionKey); - const elementId: string = existingElementId || System.createUniqueId(); - return WorldRepository.insertNewElement(userId, elementId, elementTypeId, worldId, encryptedElementName, hashedElementName, lang); - } - - /** - * Converts an element type string key to its corresponding numeric identifier. - * @param elementType - The element type key (e.g., 'laws', 'biomes', 'customs') - * @returns The numeric identifier for the element type, or 0 if not found - */ - public static getElementTypes(elementType: string): number { - return ELEMENT_TYPE_MAP[elementType] ?? 0; - } - - /** - * Removes an element from a world. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param elementId - The unique identifier of the element to remove - * @param deletedAt - The timestamp of deletion - * @param lang - The language for error messages ('fr' or 'en'), defaults to 'fr' - * @returns True if the deletion was successful, false otherwise - */ - public static removeElementFromWorld(userId: string, bookId: string, elementId: string, deletedAt: number = System.timeStampInSeconds(), lang: 'fr' | 'en' = 'fr'): boolean { - const deleted: boolean = WorldRepository.deleteElement(userId, elementId, lang); - if (deleted) { - RemovedItem.deleteTracker(userId, bookId, 'book_world_elements', elementId, deletedAt, lang); - } - return deleted; - } - -} diff --git a/electron/database/repositories/act.repository.ts b/electron/database/repositories/act.repository.ts deleted file mode 100644 index b530b0a..0000000 --- a/electron/database/repositories/act.repository.ts +++ /dev/null @@ -1,240 +0,0 @@ -import {Database, QueryResult, RunResult, SQLiteValue} from "node-sqlite3-wasm"; -import System from "../System.js"; - -export interface BookActSummariesTable extends Record { - act_sum_id: string; - book_id: string; - user_id: string; - act_index: number; - last_update: number; - summary: string | null; -} - -export interface SyncedActSummaryResult extends Record { - act_sum_id: string; - book_id: string; - last_update: number; -} - -export interface ActQuery extends Record { - act_index: number; - summary: string; -} - -export default class ActRepository { - /** - * Fetches all acts for a specific book and 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 ActQuery objects containing act index and summary. - * @throws Error if the database operation fails. - */ - public static fetchAllActs(userId: string, bookId: string, lang: 'fr' | 'en'): ActQuery[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT act_index, summary FROM book_act_summaries WHERE book_id=? AND user_id=?'; - const params: SQLiteValue[] = [bookId, userId]; - return db.all(query, params) as ActQuery[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les actes.` : `Unable to retrieve acts.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates the summary of an existing act. - * @param userId - The unique identifier of the user. - * @param bookId - The unique identifier of the book. - * @param actId - The unique identifier of the act summary. - * @param summary - The new summary text. - * @param lastUpdate - The timestamp of the last update in seconds. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if the update was successful, false otherwise. - * @throws Error if the database operation fails. - */ - public static updateActSummary(userId: string, bookId: string, actId: number, summary: string, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE book_act_summaries SET summary=?, last_update=? WHERE user_id=? AND book_id=? AND act_sum_id=?'; - const params: SQLiteValue[] = [summary, lastUpdate, userId, bookId, actId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le résumé de l'acte.` : `Unable to update act summary.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new act summary into the database. - * @param actSummaryId - The unique identifier for the new act summary. - * @param userId - The unique identifier of the user. - * @param bookId - The unique identifier of the book. - * @param actId - The act index number. - * @param actSummary - The summary text for the act. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns The act summary ID if insertion was successful. - * @throws Error if the database operation fails. - */ - static insertActSummary(actSummaryId: string, userId: string, bookId: string, actId: number, actSummary: string, lang: 'fr' | 'en'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_act_summaries (act_sum_id, book_id, user_id, act_index, summary, last_update) VALUES (?,?,?,?,?,?)'; - const params: SQLiteValue[] = [actSummaryId, bookId, userId, actId, actSummary, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter le résumé de l'acte.` : `Unable to add act summary.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult) { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du résumé de l'acte.` : `Error adding act summary.`); - } - return actSummaryId; - } - - /** - * Fetches all act summaries 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 A promise resolving to an array of BookActSummariesTable objects. - * @throws Error if the database operation fails. - */ - static async fetchBookActSummaries(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT act_sum_id, book_id, user_id, act_index, summary, last_update FROM book_act_summaries WHERE user_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - return db.all(query, params) as BookActSummariesTable[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les résumés des actes.` : `Unable to retrieve act summaries.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced act summaries 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 SyncedActSummaryResult objects containing sync metadata. - * @throws Error if the database operation fails. - */ - static fetchSyncedActSummaries(userId: string, lang: 'fr' | 'en'): SyncedActSummaryResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT act_sum_id, book_id, last_update FROM book_act_summaries WHERE user_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedActSummaries: SyncedActSummaryResult[] = db.all(query, params) as SyncedActSummaryResult[]; - return syncedActSummaries; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les résumés d'actes synchronisés.` : `Unable to retrieve synced act summaries.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a synced act summary from remote data. - * @param actSumId - The unique identifier of the act summary. - * @param bookId - The unique identifier of the book. - * @param userId - The unique identifier of the user. - * @param actIndex - The act index number. - * @param summary - The summary text (can be null). - * @param lastUpdate - The timestamp of the last update in seconds. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if the insertion was successful, false otherwise. - * @throws Error if the database operation fails. - */ - static insertSyncActSummary(actSumId: string, bookId: string, userId: string, actIndex: number, summary: string | null, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_act_summaries (act_sum_id, book_id, user_id, act_index, summary, last_update) VALUES (?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [actSumId, bookId, userId, actIndex, summary, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le résumé d'acte.` : `Unable to insert act summary.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete act summary by its unique identifier. - * @param id - The unique identifier of the act summary. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns A promise resolving to an array of BookActSummariesTable objects. - * @throws Error if the database operation fails. - */ - static async fetchCompleteActSummaryById(id: string, lang: "fr" | "en"): Promise { - try { - const db: Database = System.getDb(); - const query: string = `SELECT act_sum_id, book_id, user_id, act_index, summary, last_update - FROM book_act_summaries - WHERE act_sum_id = ?`; - const params: SQLiteValue[] = [id]; - const actSummary: BookActSummariesTable[] = db.all(query, params) as BookActSummariesTable[]; - return actSummary; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? `Impossible de récupérer le résumé d'acte complet.` : `Unable to retrieve complete act summary.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if an act summary exists for a given user, book, and act index. - * @param userId - The unique identifier of the user. - * @param bookId - The unique identifier of the book. - * @param actIndex - The act index number to check. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if the act summary exists, false otherwise. - * @throws Error if the database operation fails. - */ - static actSummarizeExist(userId: string, bookId: string, actIndex: number, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM book_act_summaries WHERE user_id =? AND book_id =? AND act_index = ?'; - const params: SQLiteValue[] = [userId, bookId, actIndex]; - const existenceCheck: QueryResult | null = db.get(query, params) || null; - return existenceCheck !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du résumé de l'acte.` : `Unable to check act summary existence.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } -} diff --git a/electron/database/repositories/book.repository.ts b/electron/database/repositories/book.repository.ts deleted file mode 100644 index b24aba3..0000000 --- a/electron/database/repositories/book.repository.ts +++ /dev/null @@ -1,495 +0,0 @@ -import {Database, QueryResult, RunResult, SQLiteValue} from 'node-sqlite3-wasm'; -import System from "../System.js"; - -export interface BookQuery extends Record { - book_id: string; - type: string; - author_id: string; - title: string; - hashed_title: string; - sub_title: string | null; - hashed_sub_title: string | null; - summary: string | null; - serie_id: number | null; - desired_release_date: string | null; - desired_word_count: number | null; - words_count: number | null; - cover_image: string | null; -} - -export interface EritBooksTable extends Record { - book_id: string; - type: string; - author_id: string; - title: string; - hashed_title: string; - sub_title: string | null; - hashed_sub_title: string | null; - summary: string | null; - serie_id: number | null; - desired_release_date: string | null; - desired_word_count: number | null; - words_count: number | null; - last_update: number; - cover_image: string | null; -} - -export interface SyncedBookResult extends Record { - book_id: string; - type: string; - title: string; - sub_title: string | null; - last_update: number; -} - -export interface BookCoverQuery extends Record { - cover_image: string; -} - -export interface BookToolsTable extends Record { - book_id: string; - user_id: string; - characters_enabled: number; - worlds_enabled: number; - locations_enabled: number; - spells_enabled: number; - last_update: number; -} - -export interface SyncedBookToolsResult extends Record { - last_update: number; - characters_enabled: number; - worlds_enabled: number; - locations_enabled: number; - spells_enabled: number; -} - -export default class BookRepo { - /** - * Retrieves all books for a user. - * @param userId - The user identifier - * @param lang - The language for error messages - * @returns List of user's books - */ - public static fetchBooks(userId: string, lang: 'fr' | 'en'): BookQuery[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT book_id, type, author_id, title, sub_title, summary, serie_id, desired_release_date, desired_word_count, words_count, cover_image FROM erit_books WHERE author_id = ? ORDER BY book_id DESC'; - const params: SQLiteValue[] = [userId]; - return db.all(query, params) as BookQuery[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(error.message); - throw new Error(lang === 'fr' ? 'Impossible de récupérer la liste des livres.' : 'Unable to retrieve book list.'); - } - console.error(error); - throw new Error(lang === 'fr' ? 'Une erreur inconnue est survenue.' : 'An unknown error occurred.'); - } - } - - /** - * Updates a book's cover image. - * @param bookId - The book identifier - * @param coverImageName - The cover image file name - * @param userId - The user identifier - * @param lang - The language for error messages - * @returns true if the update was successful - */ - public static updateBookCover(bookId: string, coverImageName: string, userId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE erit_books SET cover_image=?, last_update=? WHERE book_id=? AND author_id=?'; - const params: SQLiteValue[] = [coverImageName, System.timeStampInSeconds(), bookId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de mettre à jour la couverture du livre.' : 'Unable to update book cover.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves a book by its identifier. - * @param bookId - The book identifier - * @param userId - The user identifier - * @param lang - The language for error messages - * @returns The book information - */ - public static fetchBook(bookId: string, userId: string, lang: 'fr' | 'en'): BookQuery { - let book: BookQuery; - try { - const db: Database = System.getDb(); - const query: string = 'SELECT book_id, author_id, title, summary, sub_title, cover_image, desired_release_date, desired_word_count, words_count, serie_id FROM erit_books WHERE book_id=? AND author_id=?'; - const params: SQLiteValue[] = [bookId, userId]; - book = db.get(query, params) as BookQuery; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer les informations du livre.' : 'Unable to retrieve book information.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - if (!book) { - throw new Error(lang === 'fr' ? 'Livre non trouvé.' : 'Book not found.'); - } - return book; - } - - /** - * Verifies if a book already exists for a user. - * @param hashedTitle - The hashed book title - * @param hashedSubTitle - The hashed book subtitle - * @param userId - The user identifier - * @param lang - The language for error messages - * @returns true if the book exists - */ - public static verifyBookExist(hashedTitle: string, hashedSubTitle: string, userId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT book_id FROM erit_books WHERE hashed_title=? AND author_id=? AND hashed_sub_title=?'; - const params: SQLiteValue[] = [hashedTitle, userId, hashedSubTitle]; - const book: QueryResult | null = db.get(query, params); - return book !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible de vérifier l'existence du livre." : 'Unable to verify book existence.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Inserts a new book into the database. - * @param bookId - The book identifier - * @param userId - The user identifier - * @param encryptedTitle - The encrypted title - * @param hashedTitle - The hashed title - * @param encryptedSubTitle - The encrypted subtitle - * @param hashedSubTitle - The hashed subtitle - * @param encryptedSummary - The encrypted summary - * @param type - The book type - * @param serie - The series identifier - * @param publicationDate - The desired publication date - * @param desiredWordCount - The desired word count - * @param lang - The language for error messages - * @returns The created book identifier - */ - public static insertBook(bookId: string, userId: string, encryptedTitle: string, hashedTitle: string, encryptedSubTitle: string, hashedSubTitle: string, encryptedSummary: string, type: string, serie: number, publicationDate: string, desiredWordCount: number, lang: 'fr' | 'en'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO erit_books (book_id, type, author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)'; - const params: SQLiteValue[] = [bookId, type, userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, serie, publicationDate ? publicationDate : null, desiredWordCount, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible d'ajouter le livre." : 'Unable to add book.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? "Erreur lors de l'ajout du livre." : 'Error adding book.'); - } - return bookId; - } - - /** - * Retrieves a book's cover image. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param lang - The language for error messages - * @returns The cover information - */ - public static fetchBookCover(userId: string, bookId: string, lang: 'fr' | 'en'): BookCoverQuery { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT cover_image FROM erit_books WHERE author_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - return db.get(query, params) as BookCoverQuery; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer la couverture du livre.' : 'Unable to retrieve book cover.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Updates a book's basic information. - * @param userId - The user identifier - * @param title - The new title - * @param hashedTitle - The hashed title - * @param subTitle - The new subtitle - * @param hashedSubTitle - The hashed subtitle - * @param summary - The new summary - * @param publicationDate - The new publication date - * @param wordCount - The new desired word count - * @param bookId - The book identifier - * @param lang - The language for error messages - * @returns true if the update was successful - */ - static updateBookBasicInformation(userId: string, title: string, hashedTitle: string, subTitle: string, hashedSubTitle: string, summary: string, publicationDate: string, wordCount: number, bookId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE erit_books SET title=?, hashed_title=?, sub_title=?, hashed_sub_title=?, summary=?, serie_id=?, desired_release_date=?, desired_word_count=?, last_update=? WHERE author_id=? AND book_id=?'; - const params: SQLiteValue[] = [title, hashedTitle, subTitle, hashedSubTitle, summary, 0, publicationDate ? System.dateToMySqlDate(publicationDate) : null, wordCount, System.timeStampInSeconds(), userId, bookId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de mettre à jour les informations du livre.' : 'Unable to update book information.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Deletes a book from the database. - * @param userId - The user identifier - * @param bookId - The book identifier to delete - * @param lang - The language for error messages - * @returns true if the deletion was successful - */ - public static deleteBook(userId: string, bookId: string, lang: 'fr' | 'en'): boolean { - console.log(`Deleting book with ID ${bookId} for user ${userId}`) - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM erit_books WHERE author_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de supprimer le livre.' : 'Unable to delete book.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves all columns from erit_books table for a book. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param lang - The language for error messages - * @returns The complete book data - */ - static async fetchEritBooksTable(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT book_id, type, author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count, words_count, cover_image, last_update FROM erit_books WHERE book_id=? AND author_id=?'; - const params: SQLiteValue[] = [bookId, userId]; - return db.all(query, params) as EritBooksTable[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer les informations du livre.' : 'Unable to retrieve book information.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves synced books for a user. - * @param userId - The user identifier - * @param lang - The language for error messages - * @returns List of books with sync information - */ - static fetchSyncedBooks(userId: string, lang: 'fr' | 'en'): SyncedBookResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT book_id, type, title, sub_title, last_update FROM erit_books WHERE author_id = ?'; - const params: SQLiteValue[] = [userId]; - return db.all(query, params) as SyncedBookResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer les livres synchronisés.' : 'Unable to retrieve synced books.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Inserts a synced book from the server. - * @param bookId - The book identifier - * @param userId - The user identifier - * @param type - The book type - * @param title - The encrypted title - * @param hashedTitle - The hashed title - * @param subTitle - The encrypted subtitle - * @param hashedSubTitle - The hashed subtitle - * @param summary - The encrypted summary - * @param serieId - The series identifier - * @param desiredReleaseDate - The desired release date - * @param desiredWordCount - The desired word count - * @param wordsCount - The current word count - * @param coverImage - The cover image file name - * @param lastUpdate - The last update timestamp - * @param lang - The language for error messages - * @returns true if the insertion was successful - */ - static insertSyncBook(bookId: string, userId: string, type: string, title: string, hashedTitle: string, subTitle: string | null, hashedSubTitle: string | null, summary: string | null, serieId: number | null, desiredReleaseDate: string | null, desiredWordCount: number | null, wordsCount: number | null, coverImage: string | null, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO erit_books (book_id, author_id, type, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count, words_count, cover_image, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [bookId, userId, type, title, hashedTitle, subTitle, hashedSubTitle, summary, serieId, desiredReleaseDate, desiredWordCount, wordsCount, coverImage, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible d'insérer le livre synchronisé." : 'Unable to insert synced book.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves a complete book by its identifier (without author verification). - * @param bookId - The book identifier - * @param lang - The language for error messages - * @returns The complete book data - */ - static async fetchCompleteBookById(bookId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT * FROM erit_books WHERE book_id = ?'; - const params: SQLiteValue[] = [bookId]; - return db.all(query, params) as EritBooksTable[]; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? 'Impossible de récupérer le livre complet.' : 'Unable to retrieve complete book.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - 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, 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; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer les paramètres des outils.' : 'Unable to fetch tools settings.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - 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(); - const updateQuery: string = `UPDATE book_tools SET ${toolName}=?, last_update=? WHERE user_id=? AND book_id=?`; - const updateResult: RunResult = db.run(updateQuery, [enabledValue, lastUpdate, userId, bookId]); - if (updateResult.changes > 0) { - return true; - } - 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 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) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de mettre à jour les paramètres des outils.' : 'Unable to update tools settings.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * 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, 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, 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) { - if (error instanceof Error) { - console.error(`[BookRepository] DB Error: ${error.message}`); - } - return false; - } - } - - static fetchSyncedBookTools(userId: string, bookId: string, lang: 'fr' | 'en'): SyncedBookToolsResult | null { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT last_update, characters_enabled, worlds_enabled, locations_enabled, spells_enabled FROM book_tools WHERE user_id = ? AND book_id = ?'; - const params: SQLiteValue[] = [userId, bookId]; - const result = db.get(query, params) as SyncedBookToolsResult | undefined; - return result ?? null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer les paramètres des outils.' : 'Unable to fetch tools settings.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - static isBookExist(userId: string, bookId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM erit_books WHERE author_id = ? AND book_id = ? LIMIT 1'; - const params: SQLiteValue[] = [userId, bookId]; - const result = db.get(query, params); - return result !== undefined && result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - } - return false; - } - } - - /** - * Retrieves the series_id for a book from series_books table. - * @param bookId - The book identifier - * @param lang - The language for error messages - * @returns The series_id or null if book is not in a series - */ - static fetchBookSeriesId(bookId: string, lang: 'fr' | 'en'): string | null { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT series_id FROM series_books WHERE book_id = ? LIMIT 1'; - const params: SQLiteValue[] = [bookId]; - const result = db.get(query, params) as { series_id: string } | undefined; - return result?.series_id || null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - } - return null; - } - } -} \ No newline at end of file diff --git a/electron/database/repositories/chapter.repository.ts b/electron/database/repositories/chapter.repository.ts deleted file mode 100644 index 88983a1..0000000 --- a/electron/database/repositories/chapter.repository.ts +++ /dev/null @@ -1,755 +0,0 @@ -import {Database, QueryResult, RunResult, SQLiteValue} from 'node-sqlite3-wasm'; -import System from "../System.js"; - -export interface ChapterQueryResult extends Record { - chapter_id: string; - title: string; - chapter_order: number; -} - -export interface ActChapterQuery extends Record { - chapter_info_id: number; - chapter_id: string; - title: string; - chapter_order: number; - act_id: number; - incident_id: string | null; - plot_point_id: string | null; - summary: string; - goal: string; -} - -export interface ChapterStoryQueryResult extends Record { - chapter_info_id: number; - act_id: number; - summary: string; - chapter_summary: string; - chapter_goal: string; - incident_id: number; - incident_title: string; - incident_summary: string; - plot_point_id: number; - plot_title: string; - plot_summary: string; -} - -export interface LastChapterResult extends Record { - chapter_id: string; - version: number; -} - -export interface BookChaptersTable extends Record { - chapter_id: string; - book_id: string; - author_id: string; - title: string; - hashed_title: string; - words_count: number | null; - chapter_order: number; - last_update: number; -} - -export interface BookChapterInfosTable extends Record { - chapter_info_id: string; - chapter_id: string; - act_id: number; - incident_id: string | null; - plot_point_id: string | null; - book_id: string; - author_id: string; - summary: string | null; - goal: string | null; - last_update: number; -} - -export interface SyncedChapterResult extends Record { - chapter_id: string; - book_id: string; - title: string; - last_update: number; -} - -export interface SyncedChapterInfoResult extends Record { - chapter_info_id: string; - chapter_id: string | null; - book_id: string; - last_update: number; -} - -export interface ChapterBookResult extends Record { - title: string; - chapter_order: number; - content: string | null; -} - -export interface ChapterExportInfoResult extends Record { - chapter_id: string; - title: string; - chapter_order: number; - available_versions: string; -} - -export interface SelectedChapterContentResult extends Record { - chapter_id: string; - title: string; - chapter_order: number; - content: string; - version: number; -} - -export interface ChapterSelectionParam { - chapterId: string; - version: number; -} - -export default class ChapterRepo { - /** - * Checks if a chapter name already exists for a book. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param hashedTitle - The hashed chapter title - * @param lang - The language for error messages - * @returns true if a chapter with this name exists - */ - public static checkNameDuplication(userId: string, bookId: string, hashedTitle: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT chapter_id FROM book_chapters WHERE author_id=? AND book_id=? AND hashed_title=?'; - const params: SQLiteValue[] = [userId, bookId, hashedTitle]; - const chapter: QueryResult | null = db.get(query, params); - return chapter !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de vérifier la duplication du nom.' : 'Unable to verify name duplication.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Inserts a new chapter into the database. - * @param chapterId - The chapter identifier - * @param userId - The user identifier - * @param bookId - The book identifier - * @param title - The encrypted chapter title - * @param hashedTitle - The hashed chapter title - * @param wordsCount - The word count - * @param chapterOrder - The chapter order position - * @param lang - The language for error messages - * @returns The created chapter identifier - */ - public static insertChapter(chapterId: string, userId: string, bookId: string, title: string, hashedTitle: string, wordsCount: number, chapterOrder: number, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_chapters (chapter_id, author_id, book_id, title, hashed_title, words_count, chapter_order, last_update) VALUES (?,?,?,?,?,?,?,?)'; - const params: SQLiteValue[] = [chapterId, userId, bookId, title, hashedTitle, wordsCount, chapterOrder, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible d'ajouter le chapitre." : 'Unable to add chapter.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? "Une erreur s'est passé lors de l'ajout du chapitre." : 'Error adding chapter.'); - } - return chapterId; - } - - /** - * Retrieves all chapters with their act information for a book. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param lang - The language for error messages - * @returns List of chapters with act information - */ - public static fetchAllChapterForActs(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ActChapterQuery[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT ci.chapter_info_id AS chapter_info_id, ci.chapter_id AS chapter_id, chapter.title, chapter.chapter_order, ci.act_id, ci.incident_id AS incident_id, ci.plot_point_id AS plot_point_id, ci.summary, ci.goal FROM book_chapter_infos AS ci INNER JOIN book_chapters AS chapter ON chapter.chapter_id = ci.chapter_id WHERE ci.book_id = ? AND ci.author_id = ?'; - const params: SQLiteValue[] = [bookId, userId]; - return db.all(query, params) as ActChapterQuery[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer les chapitres pour les actes.' : 'Unable to retrieve chapters for acts.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves all chapters from a book ordered by chapter order. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param lang - The language for error messages - * @returns List of chapters - */ - public static fetchAllChapterFromABook(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterQueryResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT chapter_id, title, chapter_order FROM book_chapters WHERE book_id=? AND author_id=? ORDER BY chapter_order'; - const params: SQLiteValue[] = [bookId, userId]; - return db.all(query, params) as ChapterQueryResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer les chapitres.' : 'Unable to retrieve chapters.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Deletes a chapter from the database. - * @param userId - The user identifier - * @param chapterId - The chapter identifier to delete - * @param lang - The language for error messages - * @returns true if the deletion was successful - */ - public static deleteChapter(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM book_chapters WHERE author_id=? AND chapter_id=?'; - const params: SQLiteValue[] = [userId, chapterId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de supprimer le chapitre.' : 'Unable to delete chapter.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Inserts chapter information linking a chapter to an act. - * @param chapterInfoId - The chapter info identifier - * @param userId - The user identifier - * @param chapterId - The chapter identifier - * @param actId - The act identifier - * @param bookId - The book identifier - * @param plotId - The plot point identifier (optional) - * @param incidentId - The incident identifier (optional) - * @param lang - The language for error messages - * @returns The created chapter info identifier - */ - static insertChapterInformation(chapterInfoId: string, userId: string, chapterId: string, actId: number, bookId: string, plotId: string | null, incidentId: string | null, lang: 'fr' | 'en' = 'fr'): string { - let existingChapter: QueryResult | null; - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const checkQuery: string = 'SELECT chapter_info_id FROM book_chapter_infos WHERE chapter_id=? AND act_id=? AND book_id=? AND plot_point_id=? AND incident_id=? AND author_id=?'; - const checkParams: SQLiteValue[] = [chapterId, actId, bookId, plotId, incidentId, userId]; - existingChapter = db.get(checkQuery, checkParams); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible de vérifier l'existence de l'information du chapitre." : 'Unable to verify chapter information existence.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - if (existingChapter !== null) { - throw new Error(lang === 'fr' ? 'Le chapitre est déjà lié.' : 'Chapter is already linked.'); - } - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_chapter_infos (chapter_info_id, chapter_id, act_id, book_id, author_id, incident_id, plot_point_id, summary, goal, last_update) VALUES (?,?,?,?,?,?,?,?,?,?)'; - const params: SQLiteValue[] = [chapterInfoId, chapterId, actId, bookId, userId, incidentId, plotId, '', '', System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible d'ajouter l'information du chapitre." : 'Unable to add chapter information.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? "Une erreur s'est produite pendant la liaison du chapitre." : 'Error linking chapter.'); - } - return chapterInfoId; - } - - /** - * Updates a chapter's basic information. - * @param userId - The user identifier - * @param chapterId - The chapter identifier - * @param encryptedTitle - The encrypted title - * @param hashTitle - The hashed title - * @param chapterOrder - The chapter order position - * @param lastUpdate - The last update timestamp - * @param lang - The language for error messages - * @returns true if the update was successful - */ - public static updateChapter(userId: string, chapterId: string, encryptedTitle: string, hashTitle: string, chapterOrder: number, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE book_chapters SET title=?, hashed_title=?, chapter_order=?, last_update=? WHERE author_id=? AND chapter_id=?'; - const params: SQLiteValue[] = [encryptedTitle, hashTitle, chapterOrder, lastUpdate, userId, chapterId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de mettre à jour le chapitre.' : 'Unable to update chapter.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Updates chapter information (summary and goal). - * @param userId - The user identifier - * @param chapterId - The chapter identifier - * @param actId - The act identifier - * @param bookId - The book identifier - * @param incidentId - The incident identifier (optional) - * @param plotId - The plot point identifier (optional) - * @param summary - The chapter summary - * @param goal - The chapter goal - * @param lastUpdate - The last update timestamp - * @param lang - The language for error messages - * @returns true if the update was successful - */ - public static updateChapterInfos(userId: string, chapterId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, summary: string, goal: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - let query: string = 'UPDATE book_chapter_infos SET summary=?,goal=?,last_update=? WHERE chapter_id = ? AND act_id = ? AND book_id = ?'; - const params: SQLiteValue[] = [summary, goal, lastUpdate, chapterId, actId, bookId]; - if (incidentId) { - query += ' AND incident_id=?'; - params.push(incidentId); - } else { - query += ' AND incident_id IS NULL'; - } - if (plotId) { - query += ' AND plot_point_id=?'; - params.push(plotId); - } else { - query += ' AND plot_point_id IS NULL'; - } - query += ' AND author_id=?'; - params.push(userId); - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de mettre à jour les informations du chapitre.' : 'Unable to update chapter information.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves the last opened chapter for a book. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param lang - The language for error messages - * @returns The last chapter information or null - */ - public static fetchLastChapter(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LastChapterResult | null { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT chapter_id as chapter_id,version FROM user_last_chapter WHERE user_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - return db.get(query, params) as LastChapterResult | null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer le dernier chapitre ouvert.' : 'Unable to retrieve last opened chapter.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Updates or inserts the last chapter record for a user. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param chapterId - The chapter identifier - * @param version - The chapter version - * @param lang - The language for error messages - * @returns true if the operation was successful - */ - public static updateLastChapterRecord(userId: string, bookId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const updateQuery: string = 'UPDATE user_last_chapter SET chapter_id=?, version=? WHERE user_id=? AND book_id=?'; - const updateParams: SQLiteValue[] = [chapterId, version, userId, bookId]; - const updateResult: RunResult = db.run(updateQuery, updateParams); - if (updateResult.changes > 0) { - return true; - } - const insertQuery: string = 'INSERT INTO user_last_chapter (user_id, book_id, chapter_id, version) VALUES (?,?,?,?)'; - const insertParams: SQLiteValue[] = [userId, bookId, chapterId, version]; - const insertResult: RunResult = db.run(insertQuery, insertParams); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible d'enregistrer le dernier chapitre." : 'Unable to save last chapter.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves chapter story information including act, incident, and plot point data. - * @param userId - The user identifier - * @param chapterId - The chapter identifier - * @param lang - The language for error messages - * @returns List of chapter story information - */ - public static fetchChapterStory(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): ChapterStoryQueryResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT chapter_info_id, chapter.act_id, act_sum.summary, chapter.summary AS chapter_summary, chapter.goal AS chapter_goal, chapter.incident_id, incident.title AS incident_title, incident.summary AS incident_summary, chapter.plot_point_id, plot.title AS plot_title, plot.summary AS plot_summary FROM book_chapter_infos AS chapter LEFT JOIN book_incidents AS incident ON chapter.incident_id=incident.incident_id LEFT JOIN book_plot_points AS plot ON chapter.plot_point_id=plot.plot_point_id LEFT JOIN book_act_summaries AS act_sum ON chapter.act_id=act_sum.act_sum_id AND chapter.book_id=act_sum.book_id WHERE chapter.chapter_id=? AND chapter.author_id=?'; - const params: SQLiteValue[] = [chapterId, userId]; - return db.all(query, params) as ChapterStoryQueryResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible de récupérer l'histoire du chapitre." : 'Unable to retrieve chapter story.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Deletes chapter information by its identifier. - * @param userId - The user identifier - * @param chapterInfoId - The chapter info identifier to delete - * @param lang - The language for error messages - * @returns true if the deletion was successful - */ - static deleteChapterInformation(userId: string, chapterInfoId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM book_chapter_infos WHERE chapter_info_id=? AND author_id=?'; - const params: SQLiteValue[] = [chapterInfoId, userId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de supprimer les informations du chapitre.' : 'Unable to delete chapter information.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Checks if a chapter exists. - * @param userId - The user identifier - * @param chapterId - The chapter identifier - * @param lang - The language for error messages - * @returns true if the chapter exists - */ - static isChapterExist(userId: string, chapterId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM book_chapters WHERE chapter_id=? AND author_id=?'; - const params: SQLiteValue[] = [chapterId, userId]; - const chapter: QueryResult | null = db.get(query, params) || null; - return chapter !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible de vérifier l'existence du chapitre." : 'Unable to check chapter existence.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Checks if chapter info exists. - * @param userId - The user identifier - * @param chapterId - The chapter identifier - * @param lang - The language for error messages - * @returns true if the chapter info exists - */ - static isChapterInfoExist(userId: string, chapterId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM book_chapter_infos WHERE chapter_id=? AND author_id=?'; - const params: SQLiteValue[] = [chapterId, userId]; - const chapterInfo: QueryResult | null = db.get(query, params) || null; - return chapterInfo !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible de vérifier l'existence des informations du chapitre." : 'Unable to check chapter info existence.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves complete book chapters with their content. - * @param bookId - The book identifier - * @param lang - The language for error messages - * @returns List of chapters with content - */ - static fetchCompleteBookChapters(bookId: string, lang: 'fr' | 'en'): ChapterBookResult[] { - let chapters: ChapterBookResult[]; - try { - const db: Database = System.getDb(); - const query: string = 'SELECT title, chapter_order, content.content FROM book_chapters AS chapter LEFT JOIN book_chapter_content AS content ON chapter.chapter_id = content.chapter_id AND content.version = (SELECT MAX(version) FROM book_chapter_content WHERE chapter_id = chapter.chapter_id AND version > 1) WHERE chapter.book_id = ? ORDER BY chapter.chapter_order'; - const params: SQLiteValue[] = [bookId]; - chapters = db.all(query, params) as ChapterBookResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer les chapitres.' : 'Unable to retrieve chapters.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - if (chapters.length === 0) { - throw new Error(lang === 'fr' ? 'Aucun chapitre trouvé.' : 'No chapters found.'); - } - return chapters; - } - - /** - * Retrieves all chapters for a book. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param lang - The language for error messages - * @returns List of book chapters - */ - static async fetchBookChapters(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT chapter_id, book_id, author_id, title, hashed_title, words_count, chapter_order, last_update FROM book_chapters WHERE author_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - return db.all(query, params) as BookChaptersTable[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer les chapitres.' : 'Unable to retrieve chapters.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves chapter information for a specific chapter. - * @param userId - The user identifier - * @param chapterId - The chapter identifier - * @param lang - The language for error messages - * @returns List of chapter info records - */ - static async fetchBookChapterInfos(userId: string, chapterId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update FROM book_chapter_infos WHERE author_id=? AND chapter_id=?'; - const params: SQLiteValue[] = [userId, chapterId]; - return db.all(query, params) as BookChapterInfosTable[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer les infos des chapitres.' : 'Unable to retrieve chapter infos.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves synced chapters for a user. - * @param userId - The user identifier - * @param lang - The language for error messages - * @returns List of synced chapters - */ - static fetchSyncedChapters(userId: string, lang: 'fr' | 'en'): SyncedChapterResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT chapter_id, book_id, title, last_update FROM book_chapters WHERE author_id = ?'; - const params: SQLiteValue[] = [userId]; - return db.all(query, params) as SyncedChapterResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer les chapitres synchronisés.' : 'Unable to retrieve synced chapters.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves synced chapter infos for a user. - * @param userId - The user identifier - * @param lang - The language for error messages - * @returns List of synced chapter infos - */ - static fetchSyncedChapterInfos(userId: string, lang: 'fr' | 'en'): SyncedChapterInfoResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT chapter_info_id, chapter_id, book_id, last_update FROM book_chapter_infos WHERE author_id = ?'; - const params: SQLiteValue[] = [userId]; - return db.all(query, params) as SyncedChapterInfoResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer les infos des chapitres synchronisés.' : 'Unable to retrieve synced chapter infos.'); - } - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Inserts a synced chapter from the server. - * @param chapterId - The chapter identifier - * @param bookId - The book identifier - * @param authorId - The author identifier - * @param title - The encrypted title - * @param hashedTitle - The hashed title - * @param wordsCount - The word count - * @param chapterOrder - The chapter order - * @param lastUpdate - The last update timestamp - * @param lang - The language for error messages - * @returns true if the insertion was successful - */ - static insertSyncChapter(chapterId: string, bookId: string, authorId: string, title: string, hashedTitle: string | null, wordsCount: number | null, chapterOrder: number | null, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_chapters (chapter_id, book_id, author_id, title, hashed_title, words_count, chapter_order, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [chapterId, bookId, authorId, title, hashedTitle, wordsCount, chapterOrder, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible d'insérer le chapitre." : 'Unable to insert chapter.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Inserts synced chapter info from the server. - * @param chapterInfoId - The chapter info identifier - * @param chapterId - The chapter identifier - * @param actId - The act identifier - * @param incidentId - The incident identifier - * @param plotPointId - The plot point identifier - * @param bookId - The book identifier - * @param authorId - The author identifier - * @param summary - The chapter summary - * @param goal - The chapter goal - * @param lastUpdate - The last update timestamp - * @param lang - The language for error messages - * @returns true if the insertion was successful - */ - static insertSyncChapterInfo(chapterInfoId: string, chapterId: string, actId: number | null, incidentId: string | null, plotPointId: string | null, bookId: string, authorId: string, summary: string | null, goal: string | null, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_chapter_infos (chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [chapterInfoId, chapterId, actId, incidentId, plotPointId, bookId, authorId, summary, goal, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible d'insérer les infos du chapitre." : 'Unable to insert chapter info.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves a complete chapter by its identifier. - * @param chapterId - The chapter identifier - * @param lang - The language for error messages - * @returns The complete chapter data - */ - static async fetchCompleteChapterById(chapterId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT chapter_id, book_id, author_id, title, hashed_title, words_count, chapter_order, last_update FROM book_chapters WHERE chapter_id = ?'; - const params: SQLiteValue[] = [chapterId]; - return db.all(query, params) as BookChaptersTable[]; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? 'Impossible de récupérer le chapitre complet.' : 'Unable to retrieve complete chapter.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Retrieves complete chapter info by its identifier. - * @param chapterInfoId - The chapter info identifier - * @param lang - The language for error messages - * @returns The complete chapter info data - */ - static async fetchCompleteChapterInfoById(chapterInfoId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update FROM book_chapter_infos WHERE chapter_info_id = ?'; - const params: SQLiteValue[] = [chapterInfoId]; - return db.all(query, params) as BookChapterInfosTable[]; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? 'Impossible de récupérer les informations de chapitre complètes.' : 'Unable to retrieve complete chapter info.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - static fetchChaptersExportInfo(userId: string, bookId: string, lang: 'fr' | 'en'): ChapterExportInfoResult[] { - try { - const db: Database = System.getDb(); - const query: string = `SELECT bc.chapter_id, bc.title, bc.chapter_order, GROUP_CONCAT(DISTINCT bcc.version) AS available_versions FROM book_chapters bc LEFT JOIN book_chapter_content bcc ON bc.chapter_id = bcc.chapter_id WHERE bc.author_id = ? AND bc.book_id = ? GROUP BY bc.chapter_id, bc.title, bc.chapter_order ORDER BY bc.chapter_order`; - const params: SQLiteValue[] = [userId, bookId]; - return db.all(query, params) as ChapterExportInfoResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? "Impossible de récupérer les informations d'export des chapitres." : 'Unable to retrieve chapters export info.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - static fetchSelectedChaptersContent(bookId: string, selections: ChapterSelectionParam[], lang: 'fr' | 'en'): SelectedChapterContentResult[] { - try { - const db: Database = System.getDb(); - const conditions: string[] = selections.map((): string => '(chapter.chapter_id = ? AND content.version = ?)'); - const query: string = `SELECT chapter.chapter_id, chapter.title, chapter.chapter_order, content.content, content.version FROM book_chapters AS chapter INNER JOIN book_chapter_content AS content ON chapter.chapter_id = content.chapter_id WHERE chapter.book_id = ? AND (${conditions.join(' OR ')}) ORDER BY chapter.chapter_order`; - const params: SQLiteValue[] = [bookId]; - for (const selection of selections) { - params.push(selection.chapterId, selection.version); - } - return db.all(query, params) as SelectedChapterContentResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? 'Impossible de récupérer le contenu des chapitres sélectionnés.' : 'Unable to retrieve selected chapters content.'); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } -} diff --git a/electron/database/repositories/chaptercontent.repository.ts b/electron/database/repositories/chaptercontent.repository.ts deleted file mode 100644 index 17cdacb..0000000 --- a/electron/database/repositories/chaptercontent.repository.ts +++ /dev/null @@ -1,371 +0,0 @@ -import {Database, QueryResult, RunResult, SQLiteValue} from "node-sqlite3-wasm"; -import System from "../System.js"; - -export interface ChapterContentQueryResult extends Record { - chapter_id: string; - version: number; - content: string; - words_count: number; - title: string; - chapter_order: number; -} - -export interface ContentQueryResult extends Record { - content: string; -} - -export interface CompanionContentQueryResult extends Record { - version: number; - content: string; - words_count: number; -} - -export interface BookChapterContentTable extends Record { - content_id: string; - chapter_id: string; - author_id: string; - version: number; - content: string | null; - words_count: number; - time_on_it: number; - last_update: number; -} - -export interface SyncedChapterContentResult extends Record { - content_id: string; - chapter_id: string; - last_update: number; -} - -export default class ChapterContentRepository { - /** - * Fetches the last chapter content for a given book. - * @param userId - The ID of the user/author. - * @param bookId - The ID of the book. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns An array of chapter content results ordered by chapter order and version descending. - */ - public static fetchLastChapterContent(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterContentQueryResult[] { - try { - const db: Database = System.getDb(); - const query: string = ` - SELECT - book_chapters.chapter_id as chapter_id, - COALESCE(book_chapter_content.version, 2) AS version, - COALESCE(book_chapter_content.content, '') AS content, - COALESCE(book_chapter_content.words_count, 0) AS words_count, - book_chapters.title, - book_chapters.chapter_order - FROM book_chapters - LEFT JOIN book_chapter_content ON book_chapters.chapter_id = book_chapter_content.chapter_id - WHERE book_chapters.author_id = ? AND book_chapters.book_id = ? - ORDER BY book_chapters.chapter_order DESC, book_chapter_content.version DESC - LIMIT 1 - `; - const params: SQLiteValue[] = [userId, bookId]; - return db.all(query, params) as ChapterContentQueryResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le dernier chapitre.` : `Unable to retrieve last chapter.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates the content of a chapter. If no existing content is found, inserts a new record. - * @param userId - The ID of the user/author. - * @param chapterId - The ID of the chapter. - * @param version - The version number of the content. - * @param encryptContent - The encrypted content string. - * @param wordsCount - The word count of the content. - * @param lastUpdate - The timestamp of the last update. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if the update or insert was successful. - */ - public static updateChapterContent(userId: string, chapterId: string, version: number, encryptContent: string, wordsCount: number, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const updateQuery: string = 'UPDATE book_chapter_content SET content=?, words_count=?, last_update=? WHERE chapter_id=? AND author_id=? AND version=?'; - const updateParams: SQLiteValue[] = [encryptContent, wordsCount, lastUpdate, chapterId, userId, version]; - const updateResult: RunResult = db.run(updateQuery, updateParams); - - if (updateResult.changes > 0) { - return true; - } else { - const contentId: string = System.createUniqueId(); - const insertQuery: string = 'INSERT INTO book_chapter_content (content_id, chapter_id, author_id, version, content, words_count, last_update) VALUES (?,?,?,?,?,?,?)'; - const insertParams: SQLiteValue[] = [contentId, chapterId, userId, version, encryptContent, wordsCount, lastUpdate]; - const insertResult: RunResult = db.run(insertQuery, insertParams); - return insertResult.changes > 0; - } - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le contenu du chapitre.` : `Unable to update chapter content.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches companion content for a specific chapter and version. - * @param userId - The ID of the user/author. - * @param chapterId - The ID of the chapter. - * @param version - The version number to fetch. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns An array of companion content results. - */ - static fetchCompanionContent(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): CompanionContentQueryResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT version, content, words_count FROM book_chapter_content WHERE author_id=? AND chapter_id=? AND version=?'; - const params: SQLiteValue[] = [userId, chapterId, version]; - return db.all(query, params) as CompanionContentQueryResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le contenu compagnon.` : `Unable to retrieve companion content.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches chapter content by its order position within a book. - * @param userId - The ID of the user/author. - * @param chapterOrder - The order position of the chapter. - * @param bookId - The ID of the book. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns The content query result for the specified chapter. - * @throws Error if no chapter is found with the specified order. - */ - static fetchChapterContentByChapterOrder(userId: string, chapterOrder: number, bookId: string, lang: 'fr' | 'en' = 'fr'): ContentQueryResult { - let chapterContent: ContentQueryResult | null; - try { - const db: Database = System.getDb(); - const query: string = ` - SELECT content.content - FROM book_chapters as chapter - INNER JOIN book_chapter_content AS content ON chapter.chapter_id=content.chapter_id - WHERE chapter.chapter_order=? AND content.version=2 AND chapter.book_id=? AND chapter.author_id=? - `; - const params: SQLiteValue[] = [chapterOrder, bookId, userId]; - chapterContent = db.get(query, params) as ContentQueryResult | null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le contenu du chapitre.` : `Unable to retrieve chapter content.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!chapterContent) { - throw new Error(lang === 'fr' ? `Aucun chapitre trouvé avec cet ordre.` : `No chapter found with this order.`); - } - return chapterContent; - } - - /** - * Fetches chapter content by chapter ID and version number. - * @param userId - The ID of the user/author. - * @param chapterId - The ID of the chapter. - * @param version - The version number to fetch. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns The content query result for the specified version. - * @throws Error if no chapter is found with the specified version. - */ - static fetchChapterContentByVersion(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): ContentQueryResult { - let chapterContent: ContentQueryResult | null; - try { - const db: Database = System.getDb(); - const query: string = 'SELECT content FROM book_chapter_content WHERE author_id=? AND chapter_id=? AND version=?'; - const params: SQLiteValue[] = [userId, chapterId, version]; - chapterContent = db.get(query, params) as ContentQueryResult | null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le contenu du chapitre.` : `Unable to retrieve chapter content.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!chapterContent) { - throw new Error(lang === 'fr' ? `Aucun chapitre trouvé avec cette version.` : `No chapter found with this version.`); - } - return chapterContent; - } - - /** - * Checks whether chapter content exists for a given content ID and user. - * @param userId - The ID of the user/author. - * @param contentId - The ID of the content to check. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if the chapter content exists, false otherwise. - */ - static isChapterContentExist(userId: string, contentId: string, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM `book_chapter_content` WHERE `content_id`=? AND `author_id`=?'; - const params: SQLiteValue[] = [contentId, userId]; - const existenceCheck: QueryResult | null = db.get(query, params) || null; - return existenceCheck !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du contenu du chapitre.` : `Unable to check chapter content existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all chapter contents for a specific chapter belonging to a user. - * @param userId - The ID of the user/author. - * @param chapterId - The ID of the chapter. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns A promise resolving to an array of book chapter content records. - */ - static async fetchBookChapterContents(userId: string, chapterId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update FROM book_chapter_content WHERE author_id=? AND chapter_id=?'; - const params: SQLiteValue[] = [userId, chapterId]; - return db.all(query, params) as BookChapterContentTable[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le contenu des chapitres.` : `Unable to retrieve chapter contents.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced chapter contents for a user (content ID, chapter ID, and last update timestamp). - * @param userId - The ID of the user/author. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns An array of synced chapter content results. - */ - static fetchSyncedChapterContents(userId: string, lang: 'fr' | 'en'): SyncedChapterContentResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT content_id, chapter_id, last_update FROM book_chapter_content WHERE author_id = ?'; - const params: SQLiteValue[] = [userId]; - return db.all(query, params) as SyncedChapterContentResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le contenu des chapitres synchronisés.` : `Unable to retrieve synced chapter contents.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new chapter content record during synchronization. - * @param contentId - The unique ID for the content. - * @param chapterId - The ID of the chapter. - * @param authorId - The ID of the author. - * @param version - The version number of the content. - * @param content - The content string (can be null). - * @param wordsCount - The word count of the content. - * @param timeOnIt - The time spent on this content. - * @param lastUpdate - The timestamp of the last update. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if the insert was successful. - */ - static insertSyncChapterContent(contentId: string, chapterId: string, authorId: string, version: number, content: string | null, wordsCount: number, timeOnIt: number, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = `INSERT INTO book_chapter_content (content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`; - const params: SQLiteValue[] = [contentId, chapterId, authorId, version, content, wordsCount, timeOnIt, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le contenu du chapitre.` : `Unable to insert chapter content.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches the complete chapter content record by its content ID. - * @param contentId - The ID of the content to fetch. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns A promise resolving to an array of book chapter content records. - */ - static async fetchCompleteChapterContentById(contentId: string, lang: "fr" | "en"): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update FROM book_chapter_content WHERE content_id = ?'; - const params: SQLiteValue[] = [contentId]; - return db.all(query, params) as BookChapterContentTable[]; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? `Impossible de récupérer le contenu de chapitre complet.` : `Unable to retrieve complete chapter content.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete chapter with its content by joining chapters and chapter content tables. - * @param userId - The ID of the user/author. - * @param chapterId - The ID of the chapter. - * @param version - The version number of the content to fetch. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns The chapter content query result with chapter metadata. - * @throws Error if no chapter is found with the specified ID. - */ - public static fetchWholeChapter(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): ChapterContentQueryResult { - let wholeChapter: ChapterContentQueryResult | null; - try { - const db: Database = System.getDb(); - const query: string = ` - SELECT - chapter.chapter_id as chapter_id, - chapter.title as title, - chapter.chapter_order, - chapter.words_count, - content.content AS content, - content.version as version - FROM book_chapters AS chapter - LEFT JOIN book_chapter_content AS content ON content.chapter_id = chapter.chapter_id AND content.version = ? - WHERE chapter.chapter_id = ? AND chapter.author_id = ? - `; - const params: SQLiteValue[] = [version, chapterId, userId]; - wholeChapter = db.get(query, params) as ChapterContentQueryResult | null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le chapitre.` : `Unable to retrieve chapter.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!wholeChapter) { - throw new Error(lang === 'fr' ? `Aucun chapitre trouvé avec cet ID.` : `No chapter found with this ID.`); - } - return wholeChapter; - } -} diff --git a/electron/database/repositories/character.repository.ts b/electron/database/repositories/character.repository.ts deleted file mode 100644 index 34fef4f..0000000 --- a/electron/database/repositories/character.repository.ts +++ /dev/null @@ -1,693 +0,0 @@ -import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; -import System from "../System.js"; - -export interface BookCharactersTable extends Record { - character_id: string; - book_id: string; - user_id: string; - first_name: string; - last_name: string | null; - nickname: string | null; - age: string | null; - gender: string | null; - species: string | null; - nationality: string | null; - status: string | null; - category: string; - title: string | null; - 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 SyncedCharacterResult extends Record { - character_id: string; - book_id: string; - first_name: string; - last_update: number; -} - -export interface SyncedCharacterAttributeResult extends Record { - attr_id: string; - character_id: string; - attribute_name: string; - last_update: number; -} - -export interface BookCharactersAttributesTable extends Record { - attr_id: string; - character_id: string; - user_id: string; - attribute_name: string; - attribute_value: string; - last_update: number; -} - -export interface CharacterResult extends Record { - character_id: string; - first_name: string; - last_name: string; - nickname: string | null; - age: string | null; - gender: string | null; - species: string | null; - nationality: string | null; - status: string | null; - title: string; - category: string; - image: string; - role: string; - biography: string; - history: string; - speech_pattern: string | null; - catchphrase: string | null; - residence: string | null; - notes: string | null; - color: string | null; - series_character_id: string | null; -} - -export interface AttributeResult extends Record { - attr_id: string; - attribute_name: string; - attribute_value: string; -} - -export interface CompleteCharacterResult extends Record { - character_id: string; - first_name: string; - last_name: string; - nickname: string | null; - age: string | null; - gender: string | null; - species: string | null; - nationality: string | null; - status: string | null; - category: string; - title: string; - role: string; - biography: string; - history: string; - speech_pattern: string | null; - catchphrase: string | null; - residence: string | null; - notes: string | null; - color: string | null; - attribute_name: string; - attribute_value: string; -} - -export default class CharacterRepo { - /** - * Fetches all characters for a specific book and 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 character results - */ - public static fetchCharacters(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): CharacterResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT character_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, series_character_id FROM book_characters WHERE book_id=? AND user_id=?'; - const params: SQLiteValue[] = [bookId, userId]; - const characters: CharacterResult[] = db.all(query, params) as CharacterResult[]; - return characters; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages.` : `Unable to retrieve characters.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Adds a new character to the database. - * @param userId - The unique identifier of the user - * @param characterId - The unique identifier for the new character - * @param characterData - Object containing all encrypted character fields - * @param bookId - The unique identifier of the book - * @param lang - The language for error messages ('fr' or 'en') - * @returns The character ID if successful - */ - public static addNewCharacter(userId: string, characterId: string, characterData: { - firstName: string; - lastName: string | null; - nickname: string | null; - age: string | null; - gender: string | null; - species: string | null; - nationality: string | null; - status: string | null; - title: string | null; - category: string | null; - image: string | null; - role: string | null; - biography: string | null; - history: string | null; - speechPattern: string | null; - catchphrase: string | null; - residence: string | null; - notes: string | null; - color: string | null; - }, bookId: string, lang: 'fr' | 'en' = 'fr', seriesCharacterId: string | null = null): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = seriesCharacterId - ? 'INSERT INTO book_characters (character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, series_character_id, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)' - : 'INSERT INTO book_characters (character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'; - const params: SQLiteValue[] = seriesCharacterId - ? [characterId, bookId, userId, characterData.firstName, characterData.lastName, characterData.nickname, characterData.age, characterData.gender, characterData.species, characterData.nationality, characterData.status, characterData.category, characterData.title, characterData.image, characterData.role, characterData.biography, characterData.history, characterData.speechPattern, characterData.catchphrase, characterData.residence, characterData.notes, characterData.color, seriesCharacterId, System.timeStampInSeconds()] - : [characterId, bookId, userId, characterData.firstName, characterData.lastName, characterData.nickname, characterData.age, characterData.gender, characterData.species, characterData.nationality, characterData.status, characterData.category, characterData.title, characterData.image, characterData.role, characterData.biography, characterData.history, characterData.speechPattern, characterData.catchphrase, characterData.residence, characterData.notes, characterData.color, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter le personnage.` : `Unable to add character.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du personnage.` : `Error adding character.`); - } - return characterId; - } - - /** - * Inserts a new attribute for a character. - * @param attributeId - The unique identifier for the new attribute - * @param characterId - The unique identifier of the character - * @param userId - The unique identifier of the user - * @param type - The attribute name/type - * @param name - The attribute value - * @param lang - The language for error messages ('fr' or 'en') - * @returns The attribute ID if successful - */ - static insertAttribute(attributeId: string, characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?,?,?,?,?,?)'; - const params: SQLiteValue[] = [attributeId, characterId, userId, type, name, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter l'attribut.` : `Unable to add attribute.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'attribut.` : `Error adding attribute.`); - } - return attributeId; - } - - /** - * Updates an existing character's information. - * @param userId - The unique identifier of the user - * @param id - The unique identifier of the character to update - * @param characterData - Object containing all encrypted character fields - * @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, false otherwise - */ - static updateCharacter(userId: string, id: string, characterData: { - firstName: string; - lastName: string | null; - nickname: string | null; - age: string | null; - gender: string | null; - species: string | null; - nationality: string | null; - status: string | null; - title: string | null; - category: string | null; - image: string | null; - role: string | null; - biography: string | null; - history: string | null; - speechPattern: string | null; - catchphrase: string | null; - residence: string | null; - notes: string | null; - color: string | null; - }, lastUpdate: number, lang: 'fr' | 'en' = 'fr', seriesCharacterId: string | null = null): boolean { - try { - const db: Database = System.getDb(); - const query: string = seriesCharacterId !== null - ? 'UPDATE book_characters SET first_name=?, last_name=?, nickname=?, age=?, gender=?, species=?, nationality=?, status=?, title=?, category=?, image=?, role=?, biography=?, history=?, speech_pattern=?, catchphrase=?, residence=?, notes=?, color=?, series_character_id=?, last_update=? WHERE character_id=? AND user_id=?' - : 'UPDATE book_characters SET first_name=?, last_name=?, nickname=?, age=?, gender=?, species=?, nationality=?, status=?, title=?, category=?, image=?, role=?, biography=?, history=?, speech_pattern=?, catchphrase=?, residence=?, notes=?, color=?, last_update=? WHERE character_id=? AND user_id=?'; - const params: SQLiteValue[] = seriesCharacterId !== null - ? [characterData.firstName, characterData.lastName, characterData.nickname, characterData.age, characterData.gender, characterData.species, characterData.nationality, characterData.status, characterData.title, characterData.category, characterData.image, characterData.role, characterData.biography, characterData.history, characterData.speechPattern, characterData.catchphrase, characterData.residence, characterData.notes, characterData.color, seriesCharacterId, lastUpdate, id, userId] - : [characterData.firstName, characterData.lastName, characterData.nickname, characterData.age, characterData.gender, characterData.species, characterData.nationality, characterData.status, characterData.title, characterData.category, characterData.image, characterData.role, characterData.biography, characterData.history, characterData.speechPattern, characterData.catchphrase, characterData.residence, characterData.notes, characterData.color, lastUpdate, id, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le personnage.` : `Unable to update 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 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 - * @param attributeId - The unique identifier of the attribute to delete - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the deletion was successful, false otherwise - */ - static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM `book_characters_attributes` WHERE `attr_id`=? AND `user_id`=?'; - const params: SQLiteValue[] = [attributeId, userId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer l'attribut.` : `Unable to delete attribute.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all attributes for a specific character. - * @param characterId - The unique identifier of the character - * @param userId - The unique identifier of the user - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of attribute results - */ - static fetchAttributes(characterId: string, userId: string, lang: 'fr' | 'en' = 'fr'): AttributeResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT attr_id, attribute_name, attribute_value FROM book_characters_attributes WHERE character_id=? AND user_id=?'; - const params: SQLiteValue[] = [characterId, userId]; - const attributes: AttributeResult[] = db.all(query, params) as AttributeResult[]; - return attributes; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs.` : `Unable to retrieve attributes.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches complete character information including attributes, optionally filtered by character IDs. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param tags - An optional array of character IDs to filter by - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of complete character results with attributes - */ - static fetchCompleteCharacters(userId: string, bookId: string, tags: string[], lang: 'fr' | 'en' = 'fr'): CompleteCharacterResult[] { - try { - const db: Database = System.getDb(); - let query: string = 'SELECT charac.character_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, role, biography, history, speech_pattern, catchphrase, residence, notes, color, attribute_name, attribute_value FROM book_characters AS charac LEFT JOIN book_characters_attributes AS attr ON charac.character_id=attr.character_id WHERE charac.user_id=? AND charac.book_id=?'; - let params: SQLiteValue[] = [userId, bookId]; - if (tags && tags.length > 0) { - const placeholders: string = tags.map((): string => '?').join(','); - query += ` AND charac.character_id IN (${placeholders})`; - params.push(...tags); - } - const characters: CompleteCharacterResult[] = db.all(query, params) as CompleteCharacterResult[]; - if (characters.length === 0) { - throw new Error(lang === 'fr' ? `Aucun personnage complet trouvé.` : `No complete characters found.`); - } - return characters; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages complets.` : `Unable to retrieve complete characters.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates an existing character attribute. - * @param userId - The unique identifier of the user - * @param characterAttributeId - The unique identifier of the attribute to update - * @param attributeName - The new attribute name - * @param attributeValue - The new attribute value - * @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, false otherwise - */ - static updateCharacterAttribute(userId: string, characterAttributeId: string, attributeName: string, attributeValue: string, lastUpdate: number, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE book_characters_attributes SET attribute_name=?, attribute_value=?, last_update=? WHERE attr_id=? AND user_id=?'; - const params: SQLiteValue[] = [attributeName, attributeValue, lastUpdate, characterAttributeId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'attribut du personnage.` : `Unable to update character attribute.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a character exists in the database. - * @param userId - The unique identifier of the user - * @param characterId - The unique identifier of the character to check - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the character exists, false otherwise - */ - static isCharacterExist(userId: string, characterId: string, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM `book_characters` WHERE `character_id`=? AND `user_id`=?'; - const params: SQLiteValue[] = [characterId, userId]; - const character: QueryResult | null = db.get(query, params) || null; - return character !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du personnage.` : `Unable to check character existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a character attribute exists in the database. - * @param userId - The unique identifier of the user - * @param characterAttributeId - The unique identifier of the attribute to check - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the attribute exists, false otherwise - */ - static isCharacterAttributeExist(userId: string, characterAttributeId: string, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM `book_characters_attributes` WHERE `attr_id`=? AND `user_id`=?'; - const params: SQLiteValue[] = [characterAttributeId, userId]; - const attribute: QueryResult | null = db.get(query, params) || null; - return attribute !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'attribut du personnage.` : `Unable to check character attribute existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all characters for a specific book asynchronously. - * @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 A promise resolving to an array of book characters - */ - static async fetchBookCharacters(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = `SELECT character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, - category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update - FROM book_characters WHERE user_id=? AND book_id=?`; - const params: SQLiteValue[] = [userId, bookId]; - const characters: BookCharactersTable[] = db.all(query, params) as BookCharactersTable[]; - return characters; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages.` : `Unable to retrieve characters.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all attributes for a specific character asynchronously. - * @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 A promise resolving to an array of character attributes - */ - static async fetchBookCharactersAttributes(userId: string, characterId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM book_characters_attributes WHERE user_id=? AND character_id=?'; - const params: SQLiteValue[] = [userId, characterId]; - const attributes: BookCharactersAttributesTable[] = db.all(query, params) as BookCharactersAttributesTable[]; - return attributes; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs des personnages.` : `Unable to retrieve character attributes.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced characters 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 character results - */ - static fetchSyncedCharacters(userId: string, lang: 'fr' | 'en'): SyncedCharacterResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT character_id, book_id, first_name, last_update FROM book_characters WHERE user_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedCharacters: SyncedCharacterResult[] = db.all(query, params) as SyncedCharacterResult[]; - return syncedCharacters; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages synchronisés.` : `Unable to retrieve synced characters.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced character attributes 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 character attribute results - */ - static fetchSyncedCharacterAttributes(userId: string, lang: 'fr' | 'en'): SyncedCharacterAttributeResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT attr_id, character_id, attribute_name, last_update FROM book_characters_attributes WHERE user_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedAttributes: SyncedCharacterAttributeResult[] = db.all(query, params) as SyncedCharacterAttributeResult[]; - return syncedAttributes; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs des personnages synchronisés.` : `Unable to retrieve synced character attributes.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a synced character into the database. - * @param characterId - The unique identifier of the character - * @param bookId - The unique identifier of the book - * @param userId - The unique identifier of the user - * @param characterData - Object containing all character fields - * @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, false otherwise - */ - static insertSyncCharacter(characterId: string, bookId: string, userId: string, characterData: { - firstName: string; - lastName: string | null; - nickname: string | null; - age: string | null; - gender: string | null; - species: string | null; - nationality: string | null; - status: string | null; - category: string; - title: string | null; - image: string | null; - role: string | null; - biography: string | null; - history: string | null; - speechPattern: string | null; - catchphrase: string | null; - residence: string | null; - notes: string | null; - color: string | null; - }, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = `INSERT INTO book_characters ( - character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, - category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; - const params: SQLiteValue[] = [ - characterId, bookId, userId, - characterData.firstName, characterData.lastName, characterData.nickname, - characterData.age, characterData.gender, characterData.species, - characterData.nationality, characterData.status, characterData.category, - characterData.title, characterData.image, characterData.role, - characterData.biography, characterData.history, characterData.speechPattern, - characterData.catchphrase, characterData.residence, characterData.notes, - characterData.color, lastUpdate - ]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le personnage.` : `Unable to insert character.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a synced character attribute into the database. - * @param attrId - The unique identifier of the attribute - * @param characterId - The unique identifier of the character - * @param userId - The unique identifier of the user - * @param attributeName - The name of the attribute - * @param attributeValue - The value of the attribute - * @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, false otherwise - */ - static insertSyncCharacterAttribute(attrId: string, characterId: string, userId: string, attributeName: string, attributeValue: string, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = `INSERT INTO book_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) - VALUES (?, ?, ?, ?, ?, ?)`; - const params: SQLiteValue[] = [attrId, characterId, userId, attributeName, attributeValue, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer l'attribut du personnage.` : `Unable to insert character attribute.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete character by its ID. - * @param id - The unique identifier of the character - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of book characters (typically one) - */ - static async fetchCompleteCharacterById(id: string, lang: "fr" | "en"): Promise { - try { - const db: Database = System.getDb(); - const query: string = `SELECT character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, - category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update - FROM book_characters - WHERE character_id = ?`; - const params: SQLiteValue[] = [id]; - const character: BookCharactersTable[] = db.all(query, params) as BookCharactersTable[]; - return character; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? `Impossible de récupérer le personnage complet.` : `Unable to retrieve complete character.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete character attribute by its ID. - * @param id - The unique identifier of the attribute - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of character attributes (typically one) - */ - static async fetchCompleteCharacterAttributeById(id: string, lang: "fr" | "en"): Promise { - try { - const db: Database = System.getDb(); - const query: string = `SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update - FROM book_characters_attributes - WHERE attr_id = ?`; - const params: SQLiteValue[] = [id]; - const attribute: BookCharactersAttributesTable[] = db.all(query, params) as BookCharactersAttributesTable[]; - return attribute; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? `Impossible de récupérer l'attribut de personnage complet.` : `Unable to retrieve complete character attribute.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } -} diff --git a/electron/database/repositories/guideline.repository.ts b/electron/database/repositories/guideline.repository.ts deleted file mode 100644 index cb8694b..0000000 --- a/electron/database/repositories/guideline.repository.ts +++ /dev/null @@ -1,567 +0,0 @@ -import { Database, RunResult, SQLiteValue } from "node-sqlite3-wasm"; -import System from "../System.js"; - -export interface BookAIGuideLineTable extends Record { - user_id: string; - book_id: string; - global_resume: string | null; - themes: string | null; - verbe_tense: number | null; - narrative_type: number | null; - langue: number | null; - dialogue_type: number | null; - tone: string | null; - atmosphere: string | null; - current_resume: string | null; - last_update: number; -} - -export interface BookGuideLineTable extends Record { - user_id: string; - book_id: string; - tone: string | null; - atmosphere: string | null; - writing_style: string | null; - themes: string | null; - symbolism: string | null; - motifs: string | null; - narrative_voice: string | null; - pacing: string | null; - intended_audience: string | null; - key_messages: string | null; - last_update: number; -} - -export interface SyncedGuideLineResult extends Record { - book_id: string; - last_update: number; -} - -export interface SyncedAIGuideLineResult extends Record { - book_id: string; - last_update: number; -} - -export interface GuideLineQuery extends Record { - tone: string; - atmosphere: string; - writing_style: string; - themes: string; - symbolism: string; - motifs: string; - narrative_voice: string; - pacing: string; - intended_audience: string; - key_messages: string; -} - -export interface GuideLineAIQuery extends Record { - user_id: string; - book_id: string; - global_resume: string | null; - themes: string | null; - verbe_tense: number | null; - narrative_type: number | null; - langue: number | null; - dialogue_type: number | null; - tone: string | null; - atmosphere: string | null; - current_resume: string | null; - meta: string; -} - -export default class GuidelineRepo { - /** - * Fetches the guideline for a specific book. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of guideline query results - * @throws Error if the guideline cannot be retrieved - */ - public static fetchGuideLine(userId: string, bookId: string, lang: 'fr' | 'en'): GuideLineQuery[] { - let guidelines: GuideLineQuery[]; - try { - const db: Database = System.getDb(); - const query: string = 'SELECT * FROM book_guide_line WHERE book_id=? AND user_id=?'; - const params: SQLiteValue[] = [bookId, userId]; - guidelines = db.all(query, params) as GuideLineQuery[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer la ligne directrice.` : `Unable to retrieve guideline.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - return guidelines; - } - - /** - * Updates or inserts a guideline for a specific book. - * If the guideline exists, it updates it; otherwise, it inserts a new one. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param encryptedTone - The encrypted tone value - * @param encryptedAtmosphere - The encrypted atmosphere value - * @param encryptedWritingStyle - The encrypted writing style value - * @param encryptedThemes - The encrypted themes value - * @param encryptedSymbolism - The encrypted symbolism value - * @param encryptedMotifs - The encrypted motifs value - * @param encryptedNarrativeVoice - The encrypted narrative voice value - * @param encryptedPacing - The encrypted pacing value - * @param encryptedKeyMessages - The encrypted key messages value - * @param encryptedIntendedAudience - The encrypted intended audience value - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the operation was successful - * @throws Error if the guideline cannot be updated or inserted - */ - /** - * Updates or inserts a guideline for a specific book. - * If the guideline exists, it updates it; otherwise, it inserts a new one. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param encryptedTone - The encrypted tone value - * @param encryptedAtmosphere - The encrypted atmosphere value - * @param encryptedWritingStyle - The encrypted writing style value - * @param encryptedThemes - The encrypted themes value - * @param encryptedSymbolism - The encrypted symbolism value - * @param encryptedMotifs - The encrypted motifs value - * @param encryptedNarrativeVoice - The encrypted narrative voice value - * @param encryptedPacing - The encrypted pacing value - * @param encryptedKeyMessages - The encrypted key messages value - * @param encryptedIntendedAudience - The encrypted intended audience value - * @param lastUpdate - The timestamp of the last update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the operation was successful - * @throws Error if the guideline cannot be updated or inserted - */ - static updateGuideLine(userId: string, bookId: string, encryptedTone: string | null, encryptedAtmosphere: string | null, encryptedWritingStyle: string | null, encryptedThemes: string | null, encryptedSymbolism: string | null, encryptedMotifs: string | null, encryptedNarrativeVoice: string | null, encryptedPacing: string | null, encryptedKeyMessages: string | null, encryptedIntendedAudience: string | null, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const updateQuery: string = 'UPDATE book_guide_line SET tone=?, atmosphere=?, writing_style=?, themes=?, symbolism=?, motifs=?, narrative_voice=?, pacing=?, intended_audience=?, key_messages=?, last_update=? WHERE user_id=? AND book_id=?'; - const updateParams: SQLiteValue[] = [ - encryptedTone, - encryptedAtmosphere, - encryptedWritingStyle, - encryptedThemes, - encryptedSymbolism, - encryptedMotifs, - encryptedNarrativeVoice, - encryptedPacing, - encryptedIntendedAudience, - encryptedKeyMessages, - lastUpdate, - userId, - bookId - ]; - const updateResult: RunResult = db.run(updateQuery, updateParams); - - if (updateResult.changes > 0) { - return true; - } else { - const insertQuery: string = 'INSERT INTO book_guide_line (user_id, book_id, tone, atmosphere, writing_style, themes, symbolism, motifs, narrative_voice, pacing, intended_audience, key_messages, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)'; - const insertParams: SQLiteValue[] = [ - userId, - bookId, - encryptedTone, - encryptedAtmosphere, - encryptedWritingStyle, - encryptedThemes, - encryptedSymbolism, - encryptedMotifs, - encryptedNarrativeVoice, - encryptedPacing, - encryptedIntendedAudience, - encryptedKeyMessages, - lastUpdate - ]; - const insertResult: RunResult = db.run(insertQuery, insertParams); - return insertResult.changes > 0; - } - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour la ligne directrice.` : `Unable to update guideline.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts or updates an AI guideline for a specific book. - * If the AI guideline exists, it updates it; otherwise, it inserts a new one. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param narrativeType - The narrative type identifier - * @param dialogueType - The dialogue type identifier - * @param encryptedPlotSummary - The encrypted plot summary - * @param encryptedToneAtmosphere - The encrypted tone and atmosphere value - * @param verbTense - The verb tense identifier - * @param language - The language identifier - * @param encryptedThemes - The encrypted themes value - * @param lastUpdate - The timestamp of the last update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the operation was successful - * @throws Error if the AI guideline cannot be inserted or updated - */ - static insertAIGuideLine(userId: string, bookId: string, narrativeType: number | null, dialogueType: number | null, encryptedPlotSummary: string | null, encryptedToneAtmosphere: string | null, verbTense: number | null, language: number | null, encryptedThemes: string | null, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const updateQuery: string = 'UPDATE book_ai_guide_line SET narrative_type=?, dialogue_type=?, global_resume=?, atmosphere=?, verbe_tense=?, langue=?, themes=?, last_update=? WHERE user_id=? AND book_id=?'; - const updateParams: SQLiteValue[] = [ - narrativeType, - dialogueType, - encryptedPlotSummary, - encryptedToneAtmosphere, - verbTense, - language, - encryptedThemes, - lastUpdate, - userId, - bookId - ]; - const updateResult: RunResult = db.run(updateQuery, updateParams); - - if (updateResult.changes > 0) { - return true; - } else { - const insertQuery: string = 'INSERT INTO book_ai_guide_line (user_id, book_id, global_resume, themes, verbe_tense, narrative_type, langue, dialogue_type, tone, atmosphere, current_resume, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)'; - const insertParams: SQLiteValue[] = [ - userId, - bookId, - encryptedPlotSummary, - encryptedThemes, - verbTense, - narrativeType, - language, - dialogueType, - encryptedToneAtmosphere, - encryptedToneAtmosphere, - encryptedPlotSummary, - lastUpdate - ]; - const insertResult: RunResult = db.run(insertQuery, insertParams); - return insertResult.changes > 0; - } - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer la ligne directrice IA.` : `Unable to insert AI guideline.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches the AI guideline for a specific book. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns The AI guideline query result - * @throws Error if the AI guideline cannot be retrieved or is not found - */ - static fetchGuideLineAI(userId: string, bookId: string, lang: 'fr' | 'en'): GuideLineAIQuery { - let aiGuideline: GuideLineAIQuery | null; - try { - const db: Database = System.getDb(); - const query: string = 'SELECT narrative_type, dialogue_type, global_resume, atmosphere, verbe_tense, langue, themes, current_resume FROM book_ai_guide_line WHERE user_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - aiGuideline = db.get(query, params) as GuideLineAIQuery | null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer la ligne directrice IA.` : `Unable to retrieve AI guideline.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!aiGuideline) { - throw new Error(lang === 'fr' ? `Ligne directrice IA non trouvée.` : `AI guideline not found.`); - } - return aiGuideline; - } - - /** - * Fetches the book AI guideline table data for a specific book. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of book AI guideline table entries - * @throws Error if the AI guideline cannot be retrieved - */ - static async fetchBookAIGuideLine(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT user_id, book_id, global_resume, themes, verbe_tense, narrative_type, langue, dialogue_type, tone, atmosphere, current_resume, last_update FROM book_ai_guide_line WHERE user_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const aiGuidelines: BookAIGuideLineTable[] = db.all(query, params) as BookAIGuideLineTable[]; - return aiGuidelines; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer la ligne directrice IA.` : `Unable to retrieve AI guideline.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches the book guideline table data for a specific book. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of book guideline table entries - * @throws Error if the guideline cannot be retrieved - */ - static async fetchBookGuideLineTable(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT user_id, book_id, tone, atmosphere, writing_style, themes, symbolism, motifs, narrative_voice, pacing, intended_audience, key_messages, last_update FROM book_guide_line WHERE user_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const guidelines: BookGuideLineTable[] = db.all(query, params) as BookGuideLineTable[]; - return guidelines; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer la ligne directrice.` : `Unable to retrieve guideline.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced guidelines for a specific user. - * @param userId - The user identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of synced guideline results containing book_id and last_update - * @throws Error if the synced guidelines cannot be retrieved - */ - static fetchSyncedGuideLine(userId: string, lang: 'fr' | 'en'): SyncedGuideLineResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT book_id, last_update FROM book_guide_line WHERE user_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedGuidelines: SyncedGuideLineResult[] = db.all(query, params) as SyncedGuideLineResult[]; - return syncedGuidelines; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les lignes directrices synchronisées.` : `Unable to retrieve synced guidelines.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced AI guidelines for a specific user. - * @param userId - The user identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of synced AI guideline results containing book_id and last_update - * @throws Error if the synced AI guidelines cannot be retrieved - */ - static fetchSyncedAIGuideLine(userId: string, lang: 'fr' | 'en'): SyncedAIGuideLineResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT book_id, last_update FROM book_ai_guide_line WHERE user_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedAIGuidelines: SyncedAIGuideLineResult[] = db.all(query, params) as SyncedAIGuideLineResult[]; - return syncedAIGuidelines; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les lignes directrices IA synchronisées.` : `Unable to retrieve synced AI guidelines.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a guideline exists 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 True if the guideline exists, false otherwise. - */ - static guideLineExist(userId: string, bookId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM book_guide_line WHERE user_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const result: Record | undefined = db.get(query, params) as Record | undefined; - return result !== undefined; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de la ligne directrice.` : `Unable to check guideline existence.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if an AI guideline exists 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 True if the AI guideline exists, false otherwise. - */ - static aiGuideLineExist(userId: string, bookId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM book_ai_guide_line WHERE user_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const result: Record | undefined = db.get(query, params) as Record | undefined; - return result !== undefined; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de la ligne directrice IA.` : `Unable to check AI guideline existence.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a synced AI guideline for a specific book. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param globalResume - The global resume value (nullable) - * @param themes - The themes value (nullable) - * @param verbeTense - The verb tense identifier (nullable) - * @param narrativeType - The narrative type identifier (nullable) - * @param langue - The language identifier (nullable) - * @param dialogueType - The dialogue type identifier (nullable) - * @param tone - The tone value (nullable) - * @param atmosphere - The atmosphere value (nullable) - * @param currentResume - The current resume value (nullable) - * @param lastUpdate - The last update timestamp - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the insertion was successful - * @throws Error if the AI guideline cannot be inserted - */ - static insertSyncAIGuideLine( - userId: string, - bookId: string, - globalResume: string | null, - themes: string | null, - verbeTense: number | null, - narrativeType: number | null, - langue: number | null, - dialogueType: number | null, - tone: string | null, - atmosphere: string | null, - currentResume: string | null, - lastUpdate: number, - lang: 'fr' | 'en' - ): boolean { - try { - const db: Database = System.getDb(); - const query: string = `INSERT INTO book_ai_guide_line (user_id, book_id, global_resume, themes, verbe_tense, narrative_type, langue, dialogue_type, tone, atmosphere, current_resume, last_update) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; - const params: SQLiteValue[] = [ - userId, - bookId, - globalResume, - themes, - verbeTense, - narrativeType, - langue, - dialogueType, - tone, - atmosphere, - currentResume, - lastUpdate - ]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer la ligne directrice IA.` : `Unable to insert AI guideline.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a synced guideline for a specific book. - * @param userId - The user identifier - * @param bookId - The book identifier - * @param tone - The tone value (nullable) - * @param atmosphere - The atmosphere value (nullable) - * @param writingStyle - The writing style value (nullable) - * @param themes - The themes value (nullable) - * @param symbolism - The symbolism value (nullable) - * @param motifs - The motifs value (nullable) - * @param narrativeVoice - The narrative voice value (nullable) - * @param pacing - The pacing value (nullable) - * @param intendedAudience - The intended audience value (nullable) - * @param keyMessages - The key messages value (nullable) - * @param lastUpdate - The last update timestamp - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the insertion was successful - * @throws Error if the guideline cannot be inserted - */ - static insertSyncGuideLine( - userId: string, - bookId: string, - tone: string | null, - atmosphere: string | null, - writingStyle: string | null, - themes: string | null, - symbolism: string | null, - motifs: string | null, - narrativeVoice: string | null, - pacing: string | null, - intendedAudience: string | null, - keyMessages: string | null, - lastUpdate: number, - lang: 'fr' | 'en' - ): boolean { - try { - const db: Database = System.getDb(); - const query: string = `INSERT INTO book_guide_line (user_id, book_id, tone, atmosphere, writing_style, themes, symbolism, motifs, narrative_voice, pacing, intended_audience, key_messages, last_update) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; - const params: SQLiteValue[] = [ - userId, - bookId, - tone, - atmosphere, - writingStyle, - themes, - symbolism, - motifs, - narrativeVoice, - pacing, - intendedAudience, - keyMessages, - lastUpdate - ]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer la ligne directrice.` : `Unable to insert guideline.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } -} diff --git a/electron/database/repositories/incident.repository.ts b/electron/database/repositories/incident.repository.ts deleted file mode 100644 index 19d8dd5..0000000 --- a/electron/database/repositories/incident.repository.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { Database, QueryResult, RunResult, SQLiteValue } from "node-sqlite3-wasm"; -import System from "../System.js"; - -export interface BookIncidentsTable extends Record { - incident_id: string; - author_id: string; - book_id: string; - title: string; - hashed_title: string; - summary: string | null; - last_update: number; -} - -export interface SyncedIncidentResult extends Record { - incident_id: string; - book_id: string; - title: string; - last_update: number; -} - -export interface IncidentQuery extends Record { - incident_id: string; - title: string; - summary: string; -} - -export default class IncidentRepository { - /** - * Fetches all incidents for a specific book belonging to a user. - * @param userId - The ID of the user (author) - * @param bookId - The ID of the book - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of incidents with their ID, title, and summary - * @throws Error if the database query fails - */ - public static fetchAllIncitentIncidents(userId: string, bookId: string, lang: 'fr' | 'en'): IncidentQuery[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT incident_id, title, summary FROM book_incidents WHERE author_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const incidents: IncidentQuery[] = db.all(query, params) as IncidentQuery[]; - return incidents; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les incidents.` : `Unable to retrieve incidents.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new incident into the database. - * @param incidentId - The unique ID for the new incident - * @param userId - The ID of the user (author) - * @param bookId - The ID of the book - * @param encryptedName - The encrypted title of the incident - * @param hashedName - The hashed title of the incident - * @param lang - The language for error messages ('fr' or 'en') - * @returns The incident ID if insertion was successful - * @throws Error if the database insertion fails - */ - public static insertNewIncident(incidentId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_incidents (incident_id,author_id, book_id, title, hashed_title, last_update) VALUES (?,?,?,?,?,?)'; - const params: SQLiteValue[] = [incidentId, userId, bookId, encryptedName, hashedName, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter l'élément déclencheur.` : `Unable to add incident.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'élément déclencheur.` : `Error adding incident.`); - } - return incidentId; - } - - /** - * Deletes an incident from the database. - * @param userId - The ID of the user (author) - * @param bookId - The ID of the book - * @param incidentId - The ID of the incident to delete - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the incident was deleted, false otherwise - * @throws Error if the database deletion fails - */ - public static deleteIncident(userId: string, bookId: string, incidentId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM book_incidents WHERE author_id=? AND book_id=? AND incident_id=?'; - const params: SQLiteValue[] = [userId, bookId, incidentId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer l'élément déclencheur.` : `Unable to delete incident.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates an existing incident in the database. - * @param userId - The ID of the user (author) - * @param bookId - The ID of the book - * @param incidentId - The ID of the incident to update - * @param encryptedIncidentName - The new encrypted title - * @param incidentHashedName - The new hashed title - * @param incidentSummary - The new summary - * @param lastUpdate - The timestamp of the last update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the incident was updated, false otherwise - * @throws Error if the database update fails - */ - public static updateIncident(userId: string, bookId: string, incidentId: string, encryptedIncidentName: string, incidentHashedName: string, incidentSummary: string, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE book_incidents SET title=?, hashed_title=?, summary=?, last_update=? WHERE author_id=? AND book_id=? AND incident_id=?'; - const params: SQLiteValue[] = [encryptedIncidentName, incidentHashedName, incidentSummary, lastUpdate, userId, bookId, incidentId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'incident.` : `Unable to update incident.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all incidents for a book with complete information. - * @param userId - The ID of the user (author) - * @param bookId - The ID of the book - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of complete incident records - * @throws Error if the database query fails - */ - static async fetchBookIncidents(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT incident_id, author_id, book_id, title, hashed_title, summary, last_update FROM book_incidents WHERE author_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const incidents: BookIncidentsTable[] = db.all(query, params) as BookIncidentsTable[]; - return incidents; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les incidents.` : `Unable to retrieve incidents.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced incidents for a user across all books. - * @param userId - The ID of the user (author) - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of synced incident records with minimal information - * @throws Error if the database query fails - */ - static fetchSyncedIncidents(userId: string, lang: 'fr' | 'en'): SyncedIncidentResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT incident_id, book_id, title, last_update FROM book_incidents WHERE author_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedIncidents: SyncedIncidentResult[] = db.all(query, params) as SyncedIncidentResult[]; - return syncedIncidents; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les incidents synchronisés.` : `Unable to retrieve synced incidents.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a synced incident into the database. - * @param incidentId - The unique ID for the incident - * @param authorId - The ID of the author - * @param bookId - The ID of the book - * @param title - The encrypted title - * @param hashedTitle - The hashed title - * @param summary - The encrypted summary (can be null) - * @param lastUpdate - The timestamp of the last update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the incident was inserted, false otherwise - * @throws Error if the database insertion fails - */ - static insertSyncIncident(incidentId: string, authorId: string, bookId: string, title: string, hashedTitle: string, summary: string | null, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = `INSERT INTO book_incidents (incident_id, author_id, book_id, title, hashed_title, summary, last_update) - VALUES (?, ?, ?, ?, ?, ?, ?)`; - const params: SQLiteValue[] = [incidentId, authorId, bookId, title, hashedTitle, summary, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer l'incident.` : `Unable to insert incident.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches complete incident information by its ID. - * @param id - The ID of the incident to fetch - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array containing the incident record (empty if not found) - * @throws Error if the database query fails - */ - static async fetchCompleteIncidentById(id: string, lang: "fr" | "en"): Promise { - try { - const db: Database = System.getDb(); - const query: string = `SELECT incident_id, author_id, book_id, title, hashed_title, summary, last_update - FROM book_incidents - WHERE incident_id = ?`; - const params: SQLiteValue[] = [id]; - const incident: BookIncidentsTable[] = db.all(query, params) as BookIncidentsTable[]; - return incident; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? `Impossible de récupérer l'incident complet.` : `Unable to retrieve complete incident.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if an incident exists in the database. - * @param userId - The ID of the user (author) - * @param bookId - The ID of the book - * @param incidentId - The ID of the incident to check - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the incident exists, false otherwise - * @throws Error if the database query fails - */ - static incidentExist(userId: string, bookId: string, incidentId: string, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM book_incidents WHERE book_id=? AND incident_id=? AND author_id=?'; - const params: SQLiteValue[] = [bookId, incidentId, userId]; - const existingIncident: QueryResult | null = db.get(query, params) || null; - return existingIncident !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'incident.` : `Unable to check incident existence.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } -} diff --git a/electron/database/repositories/issue.repository.ts b/electron/database/repositories/issue.repository.ts deleted file mode 100644 index 1c846e1..0000000 --- a/electron/database/repositories/issue.repository.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { Database, QueryResult, RunResult, SQLiteValue } from "node-sqlite3-wasm"; -import System from "../System.js"; - -export interface BookIssuesTable extends Record { - issue_id: string; - author_id: string; - book_id: string; - name: string; - hashed_issue_name: string; - last_update: number; -} - -export interface SyncedIssueResult extends Record { - issue_id: string; - book_id: string; - name: string; - last_update: number; -} - -export interface IssueQuery extends Record { - issue_id: string; - name: string; -} - -export default class IssueRepository { - /** - * Fetches all issues associated with a specific book. - * @param userId - The unique identifier of the user/author. - * @param bookId - The unique identifier of the book. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns An array of issues with their IDs and names. - */ - public static fetchIssuesFromBook(userId: string, bookId: string, lang: 'fr' | 'en'): IssueQuery[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT issue_id, name FROM book_issues WHERE author_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const issues: IssueQuery[] = db.all(query, params) as IssueQuery[]; - return issues; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les problématiques.` : `Unable to retrieve issues.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new issue into the database after verifying it doesn't already exist. - * @param issueId - The unique identifier for the new issue. - * @param userId - The unique identifier of the user/author. - * @param bookId - The unique identifier of the book. - * @param encryptedName - The encrypted name of the issue. - * @param hashedName - The hashed name of the issue for duplicate checking. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns The issue ID if successfully inserted. - */ - public static insertNewIssue(issueId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en'): string { - let existingIssue: QueryResult | null; - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const checkQuery: string = 'SELECT issue_id FROM book_issues WHERE hashed_issue_name=? AND book_id=? AND author_id=?'; - const checkParams: SQLiteValue[] = [hashedName, bookId, userId]; - existingIssue = db.get(checkQuery, checkParams); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de la problématique.` : `Unable to verify issue existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (existingIssue !== null) { - throw new Error(lang === 'fr' ? `La problématique existe déjà.` : `This issue already exists.`); - } - try { - const db: Database = System.getDb(); - const insertQuery: string = 'INSERT INTO book_issues (issue_id, author_id, book_id, name, hashed_issue_name, last_update) VALUES (?, ?, ?, ?, ?, ?)'; - const insertParams: SQLiteValue[] = [issueId, userId, bookId, encryptedName, hashedName, System.timeStampInSeconds()]; - insertResult = db.run(insertQuery, insertParams); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter la problématique.` : `Unable to add issue.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Erreur pendant l'ajout de la problématique.` : `Error adding issue.`); - } - return issueId; - } - - /** - * Deletes an issue from the database. - * @param userId - The unique identifier of the user/author. - * @param issueId - The unique identifier of the issue to delete. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if the issue was successfully deleted, false otherwise. - */ - public static deleteIssue(userId: string, issueId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM book_issues WHERE author_id=? AND issue_id=?'; - const params: SQLiteValue[] = [userId, issueId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer la problématique.` : `Unable to delete issue.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all complete issue records for a specific book. - * @param userId - The unique identifier of the user/author. - * @param bookId - The unique identifier of the book. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns A promise resolving to an array of complete issue records. - */ - static async fetchBookIssues(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT issue_id, author_id, book_id, name, hashed_issue_name, last_update FROM book_issues WHERE author_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const issues: BookIssuesTable[] = db.all(query, params) as BookIssuesTable[]; - return issues; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les problématiques.` : `Unable to retrieve issues.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced issues for a specific user. - * @param userId - The unique identifier of the user/author. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns An array of synced issue records. - */ - static fetchSyncedIssues(userId: string, lang: 'fr' | 'en'): SyncedIssueResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT issue_id, book_id, name, last_update FROM book_issues WHERE author_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedIssues: SyncedIssueResult[] = db.all(query, params) as SyncedIssueResult[]; - return syncedIssues; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les problématiques synchronisées.` : `Unable to retrieve synced issues.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a synced issue from remote into the local database. - * @param issueId - The unique identifier of the issue. - * @param authorId - The unique identifier of the author. - * @param bookId - The unique identifier of the book. - * @param name - The encrypted name of the issue. - * @param hashedIssueName - The hashed name of the issue. - * @param lastUpdate - The timestamp of the last update. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if the issue was successfully inserted, false otherwise. - */ - static insertSyncIssue(issueId: string, authorId: string, bookId: string, name: string, hashedIssueName: string, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = `INSERT INTO book_issues (issue_id, author_id, book_id, name, hashed_issue_name, last_update) VALUES (?, ?, ?, ?, ?, ?)`; - const params: SQLiteValue[] = [issueId, authorId, bookId, name, hashedIssueName, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer la problématique.` : `Unable to insert issue.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete issue record by its ID. - * @param id - The unique identifier of the issue. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns A promise resolving to an array of complete issue records. - */ - static async fetchCompleteIssueById(id: string, lang: "fr" | "en"): Promise { - try { - const db: Database = System.getDb(); - const query: string = `SELECT issue_id, author_id, book_id, name, hashed_issue_name, last_update FROM book_issues WHERE issue_id = ?`; - const params: SQLiteValue[] = [id]; - const issues: BookIssuesTable[] = db.all(query, params) as BookIssuesTable[]; - return issues; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? `Impossible de récupérer le problème complet.` : `Unable to retrieve complete issue.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates an existing issue in the database. - * @param userId - The unique identifier of the user/author. - * @param bookId - The unique identifier of the book. - * @param issueId - The unique identifier of the issue to update. - * @param name - The new encrypted name of the issue. - * @param hashedName - The new hashed name of the issue. - * @param lastUpdate - The timestamp of the update. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if the issue was successfully updated, false otherwise. - */ - static updateIssue(userId: string, bookId: string, issueId: string, name: string, hashedName: string, lastUpdate: number, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = `UPDATE book_issues SET name = ?, hashed_issue_name = ?, last_update = ? WHERE issue_id = ? AND author_id = ? AND book_id = ?`; - const params: SQLiteValue[] = [name, hashedName, lastUpdate, issueId, userId, bookId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour la problématique.` : `Unable to update issue.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if an issue exists in the database. - * @param userId - The unique identifier of the user/author. - * @param bookId - The unique identifier of the book. - * @param issueId - The unique identifier of the issue to check. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if the issue exists, false otherwise. - */ - static issueExist(userId: string, bookId: string, issueId: string, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM book_issues WHERE issue_id=? AND author_id=? AND book_id=?'; - const params: SQLiteValue[] = [issueId, userId, bookId]; - const existingIssue: QueryResult | null = db.get(query, params) || null; - return existingIssue !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du problème.` : `Unable to check issue existence.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } -} diff --git a/electron/database/repositories/location.repository.ts b/electron/database/repositories/location.repository.ts deleted file mode 100644 index 152fe62..0000000 --- a/electron/database/repositories/location.repository.ts +++ /dev/null @@ -1,900 +0,0 @@ -import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; -import System from "../System.js"; - -export interface LocationQueryResult extends Record { - loc_id: string; - loc_name: string; - element_id: string; - element_name: string; - element_description: string; - sub_element_id: string; - sub_elem_name: string; - sub_elem_description: string; - series_location_id: string | null; -} - -export interface LocationElementQueryResult extends Record { - sub_element_id: string; - sub_elem_name: string; - sub_elem_description: string; - element_id: string; - element_name: string; - element_description: string; -} - -export interface LocationByTagResult extends Record { - element_name: string; - element_description: string; - sub_elem_name: string; - sub_elem_description: string; -} - -export interface BookLocationTable extends Record { - loc_id: string; - book_id: string; - user_id: string; - loc_name: string; - loc_original_name: string; - last_update: number; -} - -export interface LocationElementTable extends Record { - element_id: string; - location: string; - user_id: string; - element_name: string; - original_name: string; - element_description: string | null; - last_update: number; -} - -export interface LocationSubElementTable extends Record { - 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 SyncedLocationResult extends Record { - loc_id: string; - book_id: string; - loc_name: string; - last_update: number; -} - -export interface SyncedLocationElementResult extends Record { - element_id: string; - location: string; - element_name: string; - last_update: number; -} - -export interface SyncedLocationSubElementResult extends Record { - sub_element_id: string; - element_id: string; - sub_elem_name: string; - last_update: number; -} - -export default class LocationRepo { - /** - * Retrieves all locations with their elements and sub-elements for a specific book. - * @param userId - The user's unique identifier - * @param bookId - The book's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of location query results with nested elements - */ - static getLocation(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LocationQueryResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT loc_id, loc_name, element.element_id AS element_id, element.element_name, element.element_description, sub_elem.sub_element_id AS sub_element_id, sub_elem.sub_elem_name, sub_elem.sub_elem_description, location.series_location_id FROM book_location AS location LEFT JOIN location_element AS element ON location.loc_id = element.location LEFT JOIN location_sub_element AS sub_elem ON element.element_id = sub_elem.element_id WHERE location.user_id = ? AND location.book_id = ?'; - const params: SQLiteValue[] = [userId, bookId]; - const locations: LocationQueryResult[] = db.all(query, params) as LocationQueryResult[]; - return locations; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les emplacements.` : `Unable to retrieve locations.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new location section for a book. - * @param userId - The user's unique identifier - * @param locationId - The new location's unique identifier - * @param bookId - The book's unique identifier - * @param encryptedName - The encrypted location name - * @param originalName - The original (unencrypted) location name - * @param lang - The language for error messages ('fr' or 'en') - * @returns The location ID if insertion was successful - */ - static insertLocation(userId: string, locationId: string, bookId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr', seriesLocationId: string | null = null): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = seriesLocationId - ? 'INSERT INTO book_location (loc_id, book_id, user_id, loc_name, loc_original_name, series_location_id, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)' - : 'INSERT INTO book_location (loc_id, book_id, user_id, loc_name, loc_original_name, last_update) VALUES (?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = seriesLocationId - ? [locationId, bookId, userId, encryptedName, originalName, seriesLocationId, System.timeStampInSeconds()] - : [locationId, bookId, userId, encryptedName, originalName, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter la section d'emplacement.` : `Unable to add location section.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de la section d'emplacement.` : `Error adding location section.`); - } - return locationId; - } - - /** - * Inserts a new location element within a location section. - * @param userId - The user's unique identifier - * @param elementId - The new element's unique identifier - * @param locationId - The parent location's unique identifier - * @param encryptedName - The encrypted element name - * @param originalName - The original (unencrypted) element name - * @param lang - The language for error messages ('fr' or 'en') - * @returns The element ID if insertion was successful - */ - static insertLocationElement(userId: string, elementId: string, locationId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO location_element (element_id, location, user_id, element_name, original_name, element_description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [elementId, locationId, userId, encryptedName, originalName, '', System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter l'élément d'emplacement.` : `Unable to add location element.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'élément d'emplacement.` : `Error adding location element.`); - } - return elementId; - } - - /** - * Inserts a new sub-element within a location element. - * @param userId - The user's unique identifier - * @param subElementId - The new sub-element's unique identifier - * @param elementId - The parent element's unique identifier - * @param encryptedName - The encrypted sub-element name - * @param originalName - The original (unencrypted) sub-element name - * @param lang - The language for error messages ('fr' or 'en') - * @returns The sub-element ID if insertion was successful - */ - static insertLocationSubElement(userId: string, subElementId: string, elementId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO location_sub_element (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [subElementId, elementId, userId, encryptedName, originalName, '', System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter le sous-élément d'emplacement.` : `Unable to add location sub-element.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du sous-élément d'emplacement.` : `Error adding location sub-element.`); - } - return subElementId; - } - - /** - * Updates an existing location sub-element's name and description. - * @param userId - The user's unique identifier - * @param id - The sub-element's unique identifier - * @param encryptedName - The new encrypted sub-element name - * @param originalName - The new original (unencrypted) sub-element name - * @param encryptDescription - The new encrypted description - * @param lastUpdate - The timestamp of the last update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the update affected at least one row - */ - static updateLocationSubElement(userId: string, id: string, encryptedName: string, originalName: string, encryptDescription: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = ` - UPDATE location_sub_element - SET sub_elem_name = ?, original_name = ?, sub_elem_description = ?, last_update = ? - WHERE sub_element_id = ? AND user_id = ? - `; - const params: SQLiteValue[] = [encryptedName, originalName, encryptDescription, lastUpdate, id, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sous-élément d'emplacement.` : `Unable to update location sub-element.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates an existing location element's name and description. - * @param userId - The user's unique identifier - * @param id - The element's unique identifier - * @param encryptedName - The new encrypted element name - * @param originalName - The new original (unencrypted) element name - * @param encryptedDescription - The new encrypted description - * @param lastUpdate - The timestamp of the last update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the update affected at least one row - */ - static updateLocationElement(userId: string, id: string, encryptedName: string, originalName: string, encryptedDescription: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = ` - UPDATE location_element - SET element_name = ?, original_name = ?, element_description = ?, last_update = ? - WHERE element_id = ? AND user_id = ? - `; - const params: SQLiteValue[] = [encryptedName, originalName, encryptedDescription, lastUpdate, id, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'élément d'emplacement.` : `Unable to update location element.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates an existing location section's name. - * @param userId - The user's unique identifier - * @param id - The location section's unique identifier - * @param encryptedName - The new encrypted location name - * @param originalName - The new original (unencrypted) location name - * @param lastUpdate - The timestamp of the last update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the update affected at least one row - */ - static updateLocationSection(userId: string, id: string, encryptedName: string, originalName: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = ` - UPDATE book_location - SET loc_name = ?, loc_original_name = ?, last_update = ? - WHERE loc_id = ? AND user_id = ? - `; - const params: SQLiteValue[] = [encryptedName, originalName, lastUpdate, id, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour la section d'emplacement.` : `Unable to update location section.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Deletes a location section by its ID. - * @param userId - The user's unique identifier - * @param locationId - The location section's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the deletion affected at least one row - */ - static deleteLocationSection(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM book_location WHERE loc_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [locationId, userId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer la section d'emplacement.` : `Unable to delete location section.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Deletes a location element by its ID. - * @param userId - The user's unique identifier - * @param elementId - The element's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the deletion affected at least one row - */ - static deleteLocationElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM location_element WHERE element_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [elementId, userId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer l'élément d'emplacement.` : `Unable to delete location element.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Deletes a location sub-element by its ID. - * @param userId - The user's unique identifier - * @param subElementId - The sub-element's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the deletion affected at least one row - */ - static deleteLocationSubElement(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM location_sub_element WHERE sub_element_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [subElementId, userId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer le sous-élément d'emplacement.` : `Unable to delete location sub-element.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all location elements and sub-elements for tagging purposes. - * @param userId - The user's unique identifier - * @param bookId - The book's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of location elements with their sub-elements - */ - static fetchLocationTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LocationElementQueryResult[] { - try { - const db: Database = System.getDb(); - const query: string = ` - SELECT se.sub_element_id AS sub_element_id, se.sub_elem_name, se.sub_elem_description, - el.element_id AS element_id, el.element_name, el.element_description - FROM location_sub_element AS se - RIGHT JOIN location_element AS el ON se.element_id = el.element_id - LEFT JOIN book_location AS lo ON el.location = lo.loc_id - WHERE lo.book_id = ? AND lo.user_id = ? - `; - const params: SQLiteValue[] = [bookId, userId]; - const locationTags: LocationElementQueryResult[] = db.all(query, params) as LocationElementQueryResult[]; - return locationTags; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les tags d'emplacement.` : `Unable to retrieve location tags.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches locations by their tag IDs (element or sub-element IDs). - * @param userId - The user's unique identifier - * @param locations - An array of location tag IDs to search for - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of locations matching the provided tags - * @throws Error if no tags are provided or no locations are found - */ - static fetchLocationsByTags(userId: string, locations: string[], lang: 'fr' | 'en' = 'fr'): LocationByTagResult[] { - if (locations.length === 0) { - throw new Error(lang === 'fr' ? `Aucun tag fourni.` : `No tags provided.`); - } - try { - const db: Database = System.getDb(); - const locationPlaceholders: string = locations.map((): string => '?').join(','); - const query: string = ` - SELECT el.element_name, - el.element_description, - se.sub_elem_name, - se.sub_elem_description - FROM location_element AS el - LEFT JOIN location_sub_element AS se ON el.element_id = se.element_id - WHERE el.user_id = ? - AND (el.element_id IN (${locationPlaceholders}) OR se.sub_element_id IN (${locationPlaceholders})) - `; - const params: SQLiteValue[] = [userId, ...locations, ...locations]; - const locationsByTags: LocationByTagResult[] = db.all(query, params) as LocationByTagResult[]; - if (locationsByTags.length === 0) { - throw new Error(lang === 'fr' ? `Aucun emplacement trouvé avec ces tags.` : `No locations found with these tags.`); - } - return locationsByTags; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les emplacements par tags.` : `Unable to retrieve locations by tags.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a location exists in the database. - * @param userId - The user's unique identifier - * @param locId - The location's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the location exists, false otherwise - */ - static isLocationExist(userId: string, locId: string, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM `book_location` WHERE `loc_id` = ? AND `user_id` = ?'; - const params: SQLiteValue[] = [locId, userId]; - const existingLocation: QueryResult | null = db.get(query, params) || null; - return existingLocation !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'emplacement.` : `Unable to check location existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a location element exists in the database. - * @param userId - The user's unique identifier - * @param elementId - The element's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the location element exists, false otherwise - */ - static isLocationElementExist(userId: string, elementId: string, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM `location_element` WHERE `element_id` = ? AND `user_id` = ?'; - const params: SQLiteValue[] = [elementId, userId]; - const existingElement: QueryResult | null = db.get(query, params) || null; - return existingElement !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'élément d'emplacement.` : `Unable to check location element existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a location sub-element exists in the database. - * @param userId - The user's unique identifier - * @param subElementId - The sub-element's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the location sub-element exists, false otherwise - */ - static isLocationSubElementExist(userId: string, subElementId: string, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM `location_sub_element` WHERE `sub_element_id` = ? AND `user_id` = ?'; - const params: SQLiteValue[] = [subElementId, userId]; - const existingSubElement: QueryResult | null = db.get(query, params) || null; - return existingSubElement !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du sous-élément d'emplacement.` : `Unable to check location sub-element existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all locations for a specific book. - * @param userId - The user's unique identifier - * @param bookId - The book's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of book location records - */ - static async fetchBookLocations(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = ` - SELECT loc_id, book_id, user_id, loc_name, loc_original_name, last_update - FROM book_location - WHERE user_id = ? AND book_id = ? - `; - const params: SQLiteValue[] = [userId, bookId]; - const bookLocations: BookLocationTable[] = db.all(query, params) as BookLocationTable[]; - return bookLocations; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les lieux.` : `Unable to retrieve locations.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all elements for a specific location. - * @param userId - The user's unique identifier - * @param locationId - The location's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of location element records - */ - static async fetchLocationElements(userId: string, locationId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = ` - SELECT element_id, location, user_id, element_name, original_name, element_description, last_update - FROM location_element - WHERE user_id = ? AND location = ? - `; - const params: SQLiteValue[] = [userId, locationId]; - const locationElements: LocationElementTable[] = db.all(query, params) as LocationElementTable[]; - return locationElements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de lieu.` : `Unable to retrieve location elements.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all sub-elements for a specific location element. - * @param userId - The user's unique identifier - * @param elementId - The element's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of location sub-element records - */ - static async fetchLocationSubElements(userId: string, elementId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = ` - SELECT sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update - FROM location_sub_element - WHERE user_id = ? AND element_id = ? - `; - const params: SQLiteValue[] = [userId, elementId]; - const locationSubElements: LocationSubElementTable[] = db.all(query, params) as LocationSubElementTable[]; - return locationSubElements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments de lieu.` : `Unable to retrieve location sub-elements.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced locations for a user (used for synchronization). - * @param userId - The user's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of synced location records - */ - static fetchSyncedLocations(userId: string, lang: 'fr' | 'en'): SyncedLocationResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT loc_id, book_id, loc_name, last_update FROM book_location WHERE user_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedLocations: SyncedLocationResult[] = db.all(query, params) as SyncedLocationResult[]; - return syncedLocations; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les lieux synchronisés.` : `Unable to retrieve synced locations.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced location elements for a user (used for synchronization). - * @param userId - The user's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of synced location element records - */ - static fetchSyncedLocationElements(userId: string, lang: 'fr' | 'en'): SyncedLocationElementResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT element_id, location, element_name, last_update FROM location_element WHERE user_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedLocationElements: SyncedLocationElementResult[] = db.all(query, params) as SyncedLocationElementResult[]; - return syncedLocationElements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de lieu synchronisés.` : `Unable to retrieve synced location elements.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced location sub-elements for a user (used for synchronization). - * @param userId - The user's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of synced location sub-element records - */ - static fetchSyncedLocationSubElements(userId: string, lang: 'fr' | 'en'): SyncedLocationSubElementResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT sub_element_id, element_id, sub_elem_name, last_update FROM location_sub_element WHERE user_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedLocationSubElements: SyncedLocationSubElementResult[] = db.all(query, params) as SyncedLocationSubElementResult[]; - return syncedLocationSubElements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments de lieu synchronisés.` : `Unable to retrieve synced location sub-elements.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a synced location from the remote server. - * @param locId - The location's unique identifier - * @param bookId - The book's unique identifier - * @param userId - The user's unique identifier - * @param locName - The encrypted location name - * @param locOriginalName - The original (unencrypted) location name - * @param lastUpdate - The timestamp of the last update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the insertion affected at least one row - */ - static insertSyncLocation(locId: string, bookId: string, userId: string, locName: string, locOriginalName: string, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = ` - INSERT INTO book_location (loc_id, book_id, user_id, loc_name, loc_original_name, last_update) - VALUES (?, ?, ?, ?, ?, ?) - `; - const params: SQLiteValue[] = [locId, bookId, userId, locName, locOriginalName, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le lieu.` : `Unable to insert location.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a synced location element from the remote server. - * @param elementId - The element's unique identifier - * @param location - The parent location's unique identifier - * @param userId - The user's unique identifier - * @param elementName - The encrypted element name - * @param originalName - The original (unencrypted) element name - * @param elementDescription - The encrypted element description (can be null) - * @param lastUpdate - The timestamp of the last update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the insertion affected at least one row - */ - static insertSyncLocationElement(elementId: string, location: string, userId: string, elementName: string, originalName: string, elementDescription: string | null, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = ` - INSERT INTO location_element (element_id, location, user_id, element_name, original_name, element_description, last_update) - VALUES (?, ?, ?, ?, ?, ?, ?) - `; - const params: SQLiteValue[] = [elementId, location, userId, elementName, originalName, elementDescription, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer l'élément du lieu.` : `Unable to insert location element.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a synced location sub-element from the remote server. - * @param subElementId - The sub-element's unique identifier - * @param elementId - The parent element's unique identifier - * @param userId - The user's unique identifier - * @param subElemName - The encrypted sub-element name - * @param originalName - The original (unencrypted) sub-element name - * @param subElemDescription - The encrypted sub-element description (can be null) - * @param lastUpdate - The timestamp of the last update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the insertion affected at least one row - */ - static insertSyncLocationSubElement(subElementId: string, elementId: string, userId: string, subElemName: string, originalName: string, subElemDescription: string | null, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = ` - INSERT INTO location_sub_element (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) - VALUES (?, ?, ?, ?, ?, ?, ?) - `; - const params: SQLiteValue[] = [subElementId, elementId, userId, subElemName, originalName, subElemDescription, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le sous-élément du lieu.` : `Unable to insert location sub-element.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches complete location data by its ID (without user filtering). - * @param id - The location's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of book location records - */ - static async fetchCompleteLocationById(id: string, lang: "fr" | "en"): Promise { - try { - const db: Database = System.getDb(); - const query: string = ` - SELECT loc_id, book_id, user_id, loc_name, loc_original_name, last_update - FROM book_location - WHERE loc_id = ? - `; - const params: SQLiteValue[] = [id]; - const completeLocation: BookLocationTable[] = db.all(query, params) as BookLocationTable[]; - return completeLocation; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? `Impossible de récupérer le lieu complet.` : `Unable to retrieve complete location.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches complete location element data by its ID (without user filtering). - * @param id - The element's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of location element records - */ - static async fetchCompleteLocationElementById(id: string, lang: "fr" | "en"): Promise { - try { - const db: Database = System.getDb(); - const query: string = ` - SELECT element_id, location, user_id, element_name, original_name, element_description, last_update - FROM location_element - WHERE element_id = ? - `; - const params: SQLiteValue[] = [id]; - const completeLocationElement: LocationElementTable[] = db.all(query, params) as LocationElementTable[]; - return completeLocationElement; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? `Impossible de récupérer l'élément de lieu complet.` : `Unable to retrieve complete location element.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches complete location sub-element data by its ID (without user filtering). - * @param id - The sub-element's unique identifier - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of location sub-element records - */ - static async fetchCompleteLocationSubElementById(id: string, lang: "fr" | "en"): Promise { - try { - const db: Database = System.getDb(); - const query: string = ` - SELECT sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update - FROM location_sub_element - WHERE sub_element_id = ? - `; - const params: SQLiteValue[] = [id]; - const completeLocationSubElement: LocationSubElementTable[] = db.all(query, params) as LocationSubElementTable[]; - return completeLocationSubElement; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? `Impossible de récupérer le sous-élément de lieu complet.` : `Unable to retrieve complete location sub-element.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a location section with optional name change and series link. - * @param userId - The user's unique identifier - * @param sectionId - The section's unique identifier - * @param encryptedName - The new encrypted name (optional) - * @param originalName - The new original name (optional) - * @param seriesLocationId - The series location ID to link (optional, null to unlink) - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the update was successful - */ - static updateSectionWithSeriesLink(userId: string, sectionId: string, encryptedName: string | null, originalName: string | null, seriesLocationId: string | null, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const setClauses: string[] = ['last_update=' + System.timeStampInSeconds()]; - const params: SQLiteValue[] = []; - - if (encryptedName !== null && originalName !== null) { - setClauses.push('loc_name=?', 'loc_original_name=?'); - params.push(encryptedName, originalName); - } - - if (seriesLocationId !== undefined) { - setClauses.push('series_location_id=?'); - params.push(seriesLocationId); - } - - params.push(sectionId, userId); - - const query: string = 'UPDATE book_location SET ' + setClauses.join(', ') + ' WHERE loc_id=? AND user_id=?'; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour la section d'emplacement.` : `Unable to update location section.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } -} diff --git a/electron/database/repositories/plotpoint.repository.ts b/electron/database/repositories/plotpoint.repository.ts deleted file mode 100644 index b756a3b..0000000 --- a/electron/database/repositories/plotpoint.repository.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { Database, QueryResult, RunResult, SQLiteValue } from "node-sqlite3-wasm"; -import System from "../System.js"; - -export interface BookPlotPointsTable extends Record { - plot_point_id: string; - title: string; - hashed_title: string; - summary: string | null; - linked_incident_id: string | null; - author_id: string; - book_id: string; - last_update: number; -} - -export interface SyncedPlotPointResult extends Record { - plot_point_id: string; - book_id: string; - title: string; - last_update: number; -} - -export interface PlotPointQuery extends Record { - plot_point_id: string; - title: string; - summary: string; - linked_incident_id: string | null; -} - -export default class PlotPointRepository { - /** - * Fetches all plot points for a specific book. - * @param userId - The ID of the user/author - * @param bookId - The ID of the book - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of plot points with their basic information - */ - public static fetchAllPlotPoints(userId: string, bookId: string, lang: 'fr' | 'en'): PlotPointQuery[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT plot_point_id, title, summary, linked_incident_id FROM book_plot_points WHERE author_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const plotPoints: PlotPointQuery[] = db.all(query, params) as PlotPointQuery[]; - return plotPoints; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les points d'intrigue.` : `Unable to retrieve plot points.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new plot point into the database. - * @param plotPointId - The unique ID for the new plot point - * @param userId - The ID of the user/author - * @param bookId - The ID of the book - * @param encryptedName - The encrypted title of the plot point - * @param hashedName - The hashed title for duplicate checking - * @param incidentId - The ID of the linked incident (can be empty string) - * @param lang - The language for error messages ('fr' or 'en') - * @returns The ID of the newly created plot point - */ - static insertNewPlotPoint(plotPointId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, incidentId: string, lang: 'fr' | 'en'): string { - let existingPlotPoint: QueryResult | null; - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const checkQuery: string = 'SELECT plot_point_id FROM book_plot_points WHERE author_id=? AND book_id=? AND hashed_title=?'; - const checkParams: SQLiteValue[] = [userId, bookId, hashedName]; - existingPlotPoint = db.get(checkQuery, checkParams); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du point d'intrigue.` : `Unable to verify plot point existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (existingPlotPoint !== null) { - throw new Error(lang === 'fr' ? `Ce point de l'intrigue existe déjà.` : `This plot point already exists.`); - } - try { - const db: Database = System.getDb(); - const insertQuery: string = 'INSERT INTO book_plot_points (plot_point_id,title,hashed_title,author_id,book_id,linked_incident_id,last_update) VALUES (?,?,?,?,?,?,?)'; - const insertParams: SQLiteValue[] = [plotPointId, encryptedName, hashedName, userId, bookId, incidentId, System.timeStampInSeconds()]; - insertResult = db.run(insertQuery, insertParams); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter le point d'intrigue.` : `Unable to add plot point.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du point d'intrigue.` : `Error adding plot point.`); - } - return plotPointId; - } - - /** - * Deletes a plot point from the database. - * @param userId - The ID of the user/author - * @param plotPointId - The ID of the plot point to delete - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the plot point was deleted, false otherwise - */ - static deletePlotPoint(userId: string, plotPointId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM book_plot_points WHERE author_id=? AND plot_point_id=?'; - const params: SQLiteValue[] = [userId, plotPointId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer le point d'intrigue.` : `Unable to delete plot point.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates an existing plot point in the database. - * @param userId - The ID of the user/author - * @param bookId - The ID of the book - * @param plotPointId - The ID of the plot point to update - * @param encryptedPlotPointName - The new encrypted title - * @param plotPointHashedName - The new hashed title - * @param plotPointSummary - The new summary - * @param lastUpdate - The timestamp of the last update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the plot point was updated, false otherwise - */ - public static updatePlotPoint(userId: string, bookId: string, plotPointId: string, encryptedPlotPointName: string, plotPointHashedName: string, plotPointSummary: string, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE book_plot_points SET title=?, hashed_title=?, summary=?, last_update=? WHERE author_id=? AND book_id=? AND plot_point_id=?'; - const params: SQLiteValue[] = [encryptedPlotPointName, plotPointHashedName, plotPointSummary, lastUpdate, userId, bookId, plotPointId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le point d'intrigue.` : `Unable to update plot point.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all plot points for a book with complete information for synchronization. - * @param userId - The ID of the user/author - * @param bookId - The ID of the book - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of complete plot point records - */ - static async fetchBookPlotPoints(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT plot_point_id, title, hashed_title, summary, linked_incident_id, author_id, book_id, last_update FROM book_plot_points WHERE author_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const plotPoints: BookPlotPointsTable[] = db.all(query, params) as BookPlotPointsTable[]; - return plotPoints; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les points d'intrigue.` : `Unable to retrieve plot points.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced plot points for a user across all books. - * @param userId - The ID of the user/author - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of synced plot point records with minimal information - */ - static fetchSyncedPlotPoints(userId: string, lang: 'fr' | 'en'): SyncedPlotPointResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT plot_point_id, book_id, title, last_update FROM book_plot_points WHERE author_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedPlotPoints: SyncedPlotPointResult[] = db.all(query, params) as SyncedPlotPointResult[]; - return syncedPlotPoints; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les points d'intrigue synchronisés.` : `Unable to retrieve synced plot points.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a plot point during synchronization from remote data. - * @param plotPointId - The unique ID of the plot point - * @param title - The encrypted title - * @param hashedTitle - The hashed title for duplicate checking - * @param summary - The encrypted summary (can be null) - * @param linkedIncidentId - The ID of the linked incident (can be null) - * @param authorId - The ID of the author - * @param bookId - The ID of the book - * @param lastUpdate - The timestamp of the last update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the plot point was inserted, false otherwise - */ - static insertSyncPlotPoint(plotPointId: string, title: string, hashedTitle: string, summary: string | null, linkedIncidentId: string | null, authorId: string, bookId: string, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = `INSERT INTO book_plot_points (plot_point_id, title, hashed_title, summary, linked_incident_id, author_id, book_id, last_update) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`; - const params: SQLiteValue[] = [plotPointId, title, hashedTitle, summary, linkedIncidentId, authorId, bookId, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le point d'intrigue.` : `Unable to insert plot point.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches complete plot point data by its ID. - * @param plotPointId - The ID of the plot point to retrieve - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array containing the plot point data (empty if not found) - */ - static async fetchCompletePlotPointById(plotPointId: string, lang: "fr" | "en"): Promise { - try { - const db: Database = System.getDb(); - const query: string = `SELECT plot_point_id, title, hashed_title, summary, linked_incident_id, author_id, book_id, last_update - FROM book_plot_points - WHERE plot_point_id = ?`; - const params: SQLiteValue[] = [plotPointId]; - const plotPoint: BookPlotPointsTable[] = db.all(query, params) as BookPlotPointsTable[]; - return plotPoint; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? `Impossible de récupérer le point d'intrigue complet.` : `Unable to retrieve complete plot point.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a plot point exists in the database. - * @param userId - The ID of the user/author - * @param bookId - The ID of the book - * @param plotPointId - The ID of the plot point to check - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the plot point exists, false otherwise - */ - static plotPointExist(userId: string, bookId: string, plotPointId: string, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM book_plot_points WHERE author_id =? AND book_id =? AND plot_point_id =?'; - const params: SQLiteValue[] = [userId, bookId, plotPointId]; - const existingPlotPoint: QueryResult | null = db.get(query, params) || null; - return existingPlotPoint !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du point de intrigue.` : `Unable to check plot point existence.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } -} diff --git a/electron/database/repositories/removed-items.repository.ts b/electron/database/repositories/removed-items.repository.ts deleted file mode 100644 index 395f107..0000000 --- a/electron/database/repositories/removed-items.repository.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; -import System from '../System.js'; - -export interface RemovedItemRecord extends Record { - removal_id: string; - table_name: string; - entity_id: string; - book_id: string | null; - user_id: string; - deleted_at: number; -} - -/** - * Repository for tracking deleted items for sync purposes. - */ -export default class RemovedItemsRepository { - /** - * Inserts a removal record into the database. - * @param removalId - The unique ID for this removal record. - * @param tableName - The name of the table from which the item is deleted. - * @param entityId - The UUID of the deleted entity. - * @param bookId - Book ID (null for series items). - * @param userId - The user ID who owns the item. - * @param deletedAt - Timestamp of deletion. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if inserted successfully. - */ - public static insert( - removalId: string, - tableName: string, - entityId: string, - bookId: string | null, - userId: string, - deletedAt: number, - lang: 'fr' | 'en' - ): boolean { - try { - const db: Database = System.getDb(); - const query: string = ` - INSERT INTO removed_items (removal_id, table_name, entity_id, book_id, user_id, deleted_at) - VALUES (?, ?, ?, ?, ?, ?) - ON CONFLICT(table_name, entity_id) DO UPDATE SET deleted_at = excluded.deleted_at - `; - const params: SQLiteValue[] = [removalId, tableName, entityId, bookId, userId, deletedAt]; - const result: RunResult = db.run(query, params); - return result.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'enregistrer la suppression.` : `Unable to record deletion.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Retrieves deletions since a specific timestamp. - * Used to get deletions that occurred since last sync. - * @param userId - The user ID. - * @param since - Timestamp to get deletions after. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns Array of removed item records. - */ - public static getDeletionsSince(userId: string, since: number, lang: 'fr' | 'en'): RemovedItemRecord[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT * FROM removed_items WHERE user_id = ? AND deleted_at > ?'; - const params: SQLiteValue[] = [userId, since]; - const records: RemovedItemRecord[] = db.all(query, params) as RemovedItemRecord[]; - return records; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les suppressions.` : `Unable to retrieve deletions.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if an entity was previously deleted. - * @param tableName - The table name. - * @param entityId - The entity ID. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if the entity was deleted locally. - */ - public static wasDeleted(tableName: string, entityId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM removed_items WHERE table_name = ? AND entity_id = ? LIMIT 1'; - const params: SQLiteValue[] = [tableName, entityId]; - const result = db.get(query, params); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier si l'élément a été supprimé.` : `Unable to check if item was deleted.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Retrieves all tracked deletions for a specific book. - * @param userId - The user ID. - * @param bookId - The book ID. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns Array of removed item records for that book. - */ - public static getDeletionsForBook(userId: string, bookId: string, lang: 'fr' | 'en'): RemovedItemRecord[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT * FROM removed_items WHERE user_id = ? AND book_id = ?'; - const params: SQLiteValue[] = [userId, bookId]; - const records: RemovedItemRecord[] = db.all(query, params) as RemovedItemRecord[]; - return records; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les suppressions pour ce livre.` : `Unable to retrieve deletions for this book.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Clears all deletion records for a user. - * WARNING: Only use this when wiping user data completely. - * @param userId - The user ID. - * @param lang - The language for error messages ('fr' or 'en'). - * @returns True if cleared successfully. - */ - public static clearAllForUser(userId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM removed_items WHERE user_id = ?'; - const params: SQLiteValue[] = [userId]; - const result: RunResult = db.run(query, params); - 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 les enregistrements de suppression.` : `Unable to clear deletion records.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } -} diff --git a/electron/database/repositories/series-character.repo.ts b/electron/database/repositories/series-character.repo.ts deleted file mode 100644 index f631bc9..0000000 --- a/electron/database/repositories/series-character.repo.ts +++ /dev/null @@ -1,541 +0,0 @@ -import {Database, QueryResult, RunResult, SQLiteValue} from 'node-sqlite3-wasm'; -import System from "../System.js"; - -export interface SeriesCharacterResult extends Record { - character_id: string; - first_name: string; - last_name: string; - nickname: string | null; - age: string | null; - gender: string | null; - species: string | null; - nationality: string | null; - status: string | null; - title: string; - category: string; - image: string; - role: string; - biography: string; - history: string; - speech_pattern: string | null; - catchphrase: string | null; - residence: string | null; - notes: string | null; - color: string | null; -} - -export interface SeriesCharacterAttributeResult extends Record { - attr_id: string; - attribute_name: string; - attribute_value: string; -} - -export interface SeriesCharactersTableResult extends Record { - character_id: string; - series_id: string; - user_id: string; - first_name: string; - last_name: string | null; - nickname: string | null; - age: string | 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 SeriesCharacterAttributesTableResult extends Record { - attr_id: string; - character_id: string; - user_id: string; - attribute_name: string; - attribute_value: string; - last_update: number; -} - -export interface SyncedSeriesCharacterResult extends Record { - character_id: string; - series_id: string; - first_name: string; - last_update: number; -} - -export interface SyncedSeriesCharacterAttributeResult extends Record { - attr_id: string; - character_id: string; - attribute_name: string; - last_update: number; -} - -export default class SeriesCharacterRepo { - /** - * Fetches all characters for a specific series owned by the 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 An array of character results - */ - public static fetchCharacters(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT character_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color FROM series_characters WHERE series_id = ? AND user_id = ?'; - return db.all(query, [seriesId, userId]) as SeriesCharacterResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages de la série.` : `Unable to retrieve series characters.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Adds a new character to a series. - */ - public static addNewCharacter(userId: string, characterId: string, encryptedName: string, encryptedLastName: string | null, encryptedNickname: string | null, encryptedAge: string | null, encryptedGender: string | null, encryptedSpecies: string | null, encryptedNationality: string | null, encryptedStatus: string | null, encryptedTitle: string | null, encryptedCategory: string | null, encryptedImage: string | null, encryptedRole: string | null, encryptedBiography: string | null, encryptedHistory: string | null, encryptedSpeechPattern: string | null, encryptedCatchphrase: string | null, encryptedResidence: string | null, encryptedNotes: string | null, encryptedColor: string | null, seriesId: string, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_characters (character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [characterId, seriesId, userId, encryptedName, encryptedLastName, encryptedNickname, encryptedAge, encryptedGender, encryptedSpecies, encryptedNationality, encryptedStatus, encryptedCategory, encryptedTitle, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, encryptedSpeechPattern, encryptedCatchphrase, encryptedResidence, encryptedNotes, encryptedColor, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter le personnage.` : `Unable to add character.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du personnage.` : `Error adding character.`); - } - return characterId; - } - - /** - * Inserts a new attribute for a series character. - */ - static insertAttribute(attributeId: string, characterId: string, userId: string, type: string, name: string, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [attributeId, characterId, userId, type, name, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter l'attribut.` : `Unable to add attribute.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'attribut.` : `Error adding attribute.`); - } - return attributeId; - } - - /** - * Updates an existing series character's information. - */ - static updateCharacter(userId: string, characterId: string, encryptedName: string, encryptedLastName: string | null, encryptedNickname: string | null, encryptedAge: string | null, encryptedGender: string | null, encryptedSpecies: string | null, encryptedNationality: string | null, encryptedStatus: string | null, encryptedTitle: string | null, encryptedCategory: string | null, encryptedImage: string | null, encryptedRole: string | null, encryptedBiography: string | null, encryptedHistory: string | null, encryptedSpeechPattern: string | null, encryptedCatchphrase: string | null, encryptedResidence: string | null, encryptedNotes: string | null, encryptedColor: string | null, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_characters SET first_name = ?, last_name = ?, nickname = ?, age = ?, gender = ?, species = ?, nationality = ?, status = ?, title = ?, category = ?, image = ?, role = ?, biography = ?, history = ?, speech_pattern = ?, catchphrase = ?, residence = ?, notes = ?, color = ?, last_update = ? WHERE character_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [encryptedName, encryptedLastName, encryptedNickname, encryptedAge, encryptedGender, encryptedSpecies, encryptedNationality, encryptedStatus, encryptedTitle, encryptedCategory, encryptedImage, encryptedRole, encryptedBiography, encryptedHistory, encryptedSpeechPattern, encryptedCatchphrase, encryptedResidence, encryptedNotes, encryptedColor, System.timeStampInSeconds(), characterId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le personnage.` : `Unable to update 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 series character and all its related data via CASCADE. - */ - static deleteCharacter(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - // Delete attributes first - db.run('DELETE FROM series_characters_attributes WHERE character_id = ? AND user_id = ?', [characterId, userId]); - // Delete character - const query: string = 'DELETE FROM series_characters WHERE character_id = ? AND user_id = ?'; - const deleteResult: RunResult = db.run(query, [characterId, userId]); - return deleteResult.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 an attribute from a series character. - */ - static deleteAttribute(userId: string, attributeId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM series_characters_attributes WHERE attr_id = ? AND user_id = ?'; - const deleteResult: RunResult = db.run(query, [attributeId, userId]); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer l'attribut.` : `Unable to delete attribute.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all attributes for a specific series character. - */ - static fetchAttributes(characterId: string, userId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterAttributeResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT attr_id, attribute_name, attribute_value FROM series_characters_attributes WHERE character_id = ? AND user_id = ?'; - const attributes: SeriesCharacterAttributeResult[] = db.all(query, [characterId, userId]) as SeriesCharacterAttributeResult[]; - return attributes; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs.` : `Unable to retrieve attributes.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a series character exists. - */ - static isCharacterExist(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM series_characters WHERE character_id = ? AND user_id = ?'; - const result: QueryResult | null = db.get(query, [characterId, userId]); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du personnage.` : `Unable to check character existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all characters for a series for sync. - */ - static fetchSeriesCharactersTable(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharactersTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE series_id = ? AND user_id = ?'; - const characters: SeriesCharactersTableResult[] = db.all(query, [seriesId, userId]) as SeriesCharactersTableResult[]; - return characters; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages pour sync.` : `Unable to retrieve characters for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all attributes for a character for sync. - */ - static fetchSeriesCharacterAttributesTable(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterAttributesTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM series_characters_attributes WHERE character_id = ? AND user_id = ?'; - const attributes: SeriesCharacterAttributesTableResult[] = db.all(query, [characterId, userId]) as SeriesCharacterAttributesTableResult[]; - return attributes; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs pour sync.` : `Unable to retrieve attributes for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all series characters for a user for sync comparison. - */ - static fetchSyncedSeriesCharacters(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesCharacterResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT character_id, series_id, first_name, last_update FROM series_characters WHERE user_id = ?'; - const characters: SyncedSeriesCharacterResult[] = db.all(query, [userId]) as SyncedSeriesCharacterResult[]; - return characters; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages de série pour sync.` : `Unable to retrieve series characters for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all series character attributes for a user for sync comparison. - */ - static fetchSyncedSeriesCharacterAttributes(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesCharacterAttributeResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT attr_id, character_id, attribute_name, last_update FROM series_characters_attributes WHERE user_id = ?'; - const attributes: SyncedSeriesCharacterAttributeResult[] = db.all(query, [userId]) as SyncedSeriesCharacterAttributeResult[]; - return attributes; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs de personnage pour sync.` : `Unable to retrieve character attributes for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete character by ID for sync. - */ - static fetchCompleteCharacterById(characterId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharactersTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE character_id = ?'; - const characters: SeriesCharactersTableResult[] = db.all(query, [characterId]) as SeriesCharactersTableResult[]; - return characters; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le personnage complet.` : `Unable to retrieve complete character.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete character attribute by ID for sync. - */ - static fetchCompleteAttributeById(attrId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterAttributesTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM series_characters_attributes WHERE attr_id = ?'; - const attributes: SeriesCharacterAttributesTableResult[] = db.all(query, [attrId]) as SeriesCharacterAttributesTableResult[]; - return attributes; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer l'attribut complet.` : `Unable to retrieve complete attribute.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series character for sync. - */ - static insertSyncSeriesCharacter(characterId: string, seriesId: string, userId: string, firstName: string, lastName: string | null, nickname: string | null, age: string | null, gender: string | null, species: string | null, nationality: string | null, status: string | null, category: string, title: string | null, image: string | null, role: string | null, biography: string | null, history: string | null, speechPattern: string | null, catchphrase: string | null, residence: string | null, notes: string | null, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_characters (character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(character_id) DO UPDATE SET first_name = excluded.first_name, last_name = excluded.last_name, nickname = excluded.nickname, age = excluded.age, gender = excluded.gender, species = excluded.species, nationality = excluded.nationality, status = excluded.status, category = excluded.category, title = excluded.title, image = excluded.image, role = excluded.role, biography = excluded.biography, history = excluded.history, speech_pattern = excluded.speech_pattern, catchphrase = excluded.catchphrase, residence = excluded.residence, notes = excluded.notes, color = excluded.color, last_update = excluded.last_update'; - const params: SQLiteValue[] = [characterId, seriesId, userId, firstName, lastName, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speechPattern, catchphrase, residence, notes, color, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le personnage pour sync.` : `Unable to insert character for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a series character for sync. - */ - static updateSyncSeriesCharacter(userId: string, characterId: string, firstName: string, lastName: string | null, nickname: string | null, age: string | null, gender: string | null, species: string | null, nationality: string | null, status: string | null, category: string, title: string | null, image: string | null, role: string | null, biography: string | null, history: string | null, speechPattern: string | null, catchphrase: string | null, residence: string | null, notes: string | null, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_characters SET first_name = ?, last_name = ?, nickname = ?, age = ?, gender = ?, species = ?, nationality = ?, status = ?, category = ?, title = ?, image = ?, role = ?, biography = ?, history = ?, speech_pattern = ?, catchphrase = ?, residence = ?, notes = ?, color = ?, last_update = ? WHERE character_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [firstName, lastName, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speechPattern, catchphrase, residence, notes, color, lastUpdate, characterId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le personnage pour sync.` : `Unable to update character for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series character attribute for sync. - */ - static insertSyncSeriesCharacterAttribute(attrId: string, characterId: string, userId: string, attributeName: string, attributeValue: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(attr_id) DO UPDATE SET attribute_name = excluded.attribute_name, attribute_value = excluded.attribute_value, last_update = excluded.last_update'; - const params: SQLiteValue[] = [attrId, characterId, userId, attributeName, attributeValue, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer l'attribut pour sync.` : `Unable to insert attribute for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a series character attribute exists. - */ - static isAttributeExist(userId: string, attrId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM series_characters_attributes WHERE attr_id = ? AND user_id = ?'; - const result: QueryResult | null = db.get(query, [attrId, userId]); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'attribut.` : `Unable to check attribute existence.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a series character attribute for sync. - */ - static updateSyncSeriesCharacterAttribute(userId: string, attrId: string, attributeName: string, attributeValue: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_characters_attributes SET attribute_name = ?, attribute_value = ?, last_update = ? WHERE attr_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [attributeName, attributeValue, lastUpdate, attrId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'attribut pour sync.` : `Unable to update attribute for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - static fetchCharactersTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharactersTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE series_id = ?'; - return db.all(query, [seriesId]) as SeriesCharactersTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les personnages pour sync.` : `Unable to retrieve characters for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - static fetchCharacterAttributesTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterAttributesTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT sca.attr_id, sca.character_id, sca.user_id, sca.attribute_name, sca.attribute_value, sca.last_update FROM series_characters_attributes sca INNER JOIN series_characters sc ON sca.character_id = sc.character_id WHERE sc.series_id = ?'; - return db.all(query, [seriesId]) as SeriesCharacterAttributesTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs pour sync.` : `Unable to retrieve attributes for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all characters for a series (alias for fetchCharacters that returns full table result). - */ - static fetchSeriesCharacters(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharactersTableResult[] { - return this.fetchSeriesCharactersTable(userId, seriesId, lang); - } - - /** - * Fetches all character attributes for a series by series ID. - */ - static fetchSeriesCharacterAttributesBySeriesId(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesCharacterAttributesTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT sca.attr_id, sca.character_id, sca.user_id, sca.attribute_name, sca.attribute_value, sca.last_update FROM series_characters_attributes sca INNER JOIN series_characters sc ON sca.character_id = sc.character_id WHERE sc.series_id = ? AND sc.user_id = ?'; - return db.all(query, [seriesId, userId]) as SeriesCharacterAttributesTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les attributs par série.` : `Unable to retrieve attributes by series.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a series character exists (alias for isCharacterExist). - */ - static seriesCharacterExists(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): boolean { - return this.isCharacterExist(userId, characterId, lang); - } - - /** - * Checks if a series character attribute exists (alias for isAttributeExist). - */ - static seriesCharacterAttributeExists(userId: string, attrId: string, lang: 'fr' | 'en' = 'fr'): boolean { - return this.isAttributeExist(userId, attrId, lang); - } -} diff --git a/electron/database/repositories/series-location.repo.ts b/electron/database/repositories/series-location.repo.ts deleted file mode 100644 index 9d89194..0000000 --- a/electron/database/repositories/series-location.repo.ts +++ /dev/null @@ -1,813 +0,0 @@ -import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; -import System from "../System.js"; - -export interface SeriesLocationResult extends Record { - loc_id: string; - loc_name: string; -} - -export interface SeriesLocationElementResult extends Record { - element_id: string; - location_id: string; - element_name: string; - element_description: string; -} - -export interface SeriesLocationSubElementResult extends Record { - sub_element_id: string; - element_id: string; - sub_elem_name: string; - sub_elem_description: string; -} - -export interface SeriesLocationsTableResult extends Record { - loc_id: string; - series_id: string; - user_id: string; - loc_name: string; - loc_original_name: string; - last_update: number; -} - -export interface SeriesLocationElementsTableResult extends Record { - element_id: string; - location_id: string; - user_id: string; - element_name: string; - original_name: string; - element_description: string | null; - last_update: number; -} - -export interface SeriesLocationSubElementsTableResult extends Record { - 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 SyncedSeriesLocationResult extends Record { - loc_id: string; - series_id: string; - loc_name: string; - last_update: number; -} - -export interface SyncedSeriesLocationElementResult extends Record { - element_id: string; - location_id: string; - element_name: string; - last_update: number; -} - -export interface SyncedSeriesLocationSubElementResult extends Record { - sub_element_id: string; - element_id: string; - sub_elem_name: string; - last_update: number; -} - -export default class SeriesLocationRepo { - /** - * Fetches all locations for a series. - */ - public static fetchLocations(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT loc_id, loc_name FROM series_locations WHERE user_id = ? AND series_id = ?'; - const locations: SeriesLocationResult[] = db.all(query, [userId, seriesId]) as SeriesLocationResult[]; - return locations; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les lieux.` : `Unable to retrieve locations.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all elements for a location. - */ - public static fetchElements(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationElementResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT element_id, location_id, element_name, element_description FROM series_location_elements WHERE user_id = ? AND location_id = ?'; - const elements: SeriesLocationElementResult[] = db.all(query, [userId, locationId]) as SeriesLocationElementResult[]; - return elements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments.` : `Unable to retrieve elements.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all sub-elements for an element. - */ - public static fetchSubElements(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationSubElementResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT sub_element_id, element_id, sub_elem_name, sub_elem_description FROM series_location_sub_elements WHERE user_id = ? AND element_id = ?'; - const subElements: SeriesLocationSubElementResult[] = db.all(query, [userId, elementId]) as SeriesLocationSubElementResult[]; - return subElements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments.` : `Unable to retrieve sub-elements.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new location section. - */ - public static insertLocation(locationId: string, seriesId: string, userId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_locations (loc_id, series_id, user_id, loc_name, loc_original_name, last_update) VALUES (?, ?, ?, ?, ?, ?)'; - insertResult = db.run(query, [locationId, seriesId, userId, encryptedName, originalName, System.timeStampInSeconds()]); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter le lieu.` : `Unable to add location.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du lieu.` : `Error adding location.`); - } - return locationId; - } - - /** - * Inserts a new element. - */ - public static insertElement(elementId: string, locationId: string, userId: string, encryptedName: string, originalName: string, description: string | null, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_location_elements (element_id, location_id, user_id, element_name, original_name, element_description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)'; - insertResult = db.run(query, [elementId, locationId, userId, encryptedName, originalName, description, System.timeStampInSeconds()]); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter l'élément.` : `Unable to add element.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout de l'élément.` : `Error adding element.`); - } - return elementId; - } - - /** - * Inserts a new sub-element. - */ - public static insertSubElement(subElementId: string, elementId: string, userId: string, encryptedName: string, originalName: string, description: string | null, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_location_sub_elements (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)'; - insertResult = db.run(query, [subElementId, elementId, userId, encryptedName, originalName, description, System.timeStampInSeconds()]); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter le sous-élément.` : `Unable to add sub-element.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du sous-élément.` : `Error adding sub-element.`); - } - return subElementId; - } - - /** - * Deletes a location section. - */ - public static deleteLocation(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM series_locations WHERE loc_id = ? AND user_id = ?'; - const deleteResult: RunResult = db.run(query, [locationId, userId]); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer le lieu.` : `Unable to delete location.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Deletes an element. - */ - public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM series_location_elements WHERE element_id = ? AND user_id = ?'; - const deleteResult: RunResult = db.run(query, [elementId, userId]); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer l'élément.` : `Unable to delete element.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Deletes a sub-element. - */ - public static deleteSubElement(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM series_location_sub_elements WHERE sub_element_id = ? AND user_id = ?'; - const deleteResult: RunResult = db.run(query, [subElementId, userId]); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer le sous-élément.` : `Unable to delete sub-element.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a location's name. - */ - public static updateLocation(userId: string, locationId: string, encryptedName: string, originalName: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_locations SET loc_name = ?, loc_original_name = ?, last_update = ? WHERE loc_id = ? AND user_id = ?'; - const updateResult: RunResult = db.run(query, [encryptedName, originalName, System.timeStampInSeconds(), locationId, userId]); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le lieu.` : `Unable to update location.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all locations for a series for sync. - */ - public static fetchSeriesLocationsTable(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT loc_id, series_id, user_id, loc_name, loc_original_name, last_update FROM series_locations WHERE series_id = ? AND user_id = ?'; - const locations: SeriesLocationsTableResult[] = db.all(query, [seriesId, userId]) as SeriesLocationsTableResult[]; - return locations; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les lieux pour sync.` : `Unable to retrieve locations for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all elements for a location for sync. - */ - public static fetchSeriesLocationElementsTable(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationElementsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT element_id, location_id, user_id, element_name, original_name, element_description, last_update FROM series_location_elements WHERE location_id = ? AND user_id = ?'; - const elements: SeriesLocationElementsTableResult[] = db.all(query, [locationId, userId]) as SeriesLocationElementsTableResult[]; - return elements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de lieu pour sync.` : `Unable to retrieve location elements for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all sub-elements for an element for sync. - */ - public static fetchSeriesLocationSubElementsTable(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationSubElementsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update FROM series_location_sub_elements WHERE element_id = ? AND user_id = ?'; - const subElements: SeriesLocationSubElementsTableResult[] = db.all(query, [elementId, userId]) as SeriesLocationSubElementsTableResult[]; - return subElements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments pour sync.` : `Unable to retrieve sub-elements for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all series locations for a user for sync comparison. - */ - public static fetchSyncedSeriesLocations(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesLocationResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT loc_id, series_id, loc_name, last_update FROM series_locations WHERE user_id = ?'; - const locations: SyncedSeriesLocationResult[] = db.all(query, [userId]) as SyncedSeriesLocationResult[]; - return locations; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les lieux de série pour sync.` : `Unable to retrieve series locations for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all series location elements for a user for sync comparison. - */ - public static fetchSyncedSeriesLocationElements(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesLocationElementResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT element_id, location_id, element_name, last_update FROM series_location_elements WHERE user_id = ?'; - const elements: SyncedSeriesLocationElementResult[] = db.all(query, [userId]) as SyncedSeriesLocationElementResult[]; - return elements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de lieu pour sync.` : `Unable to retrieve location elements for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all series location sub-elements for a user for sync comparison. - */ - public static fetchSyncedSeriesLocationSubElements(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesLocationSubElementResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT sub_element_id, element_id, sub_elem_name, last_update FROM series_location_sub_elements WHERE user_id = ?'; - const subElements: SyncedSeriesLocationSubElementResult[] = db.all(query, [userId]) as SyncedSeriesLocationSubElementResult[]; - return subElements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments de lieu pour sync.` : `Unable to retrieve location sub-elements for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete location by ID for sync. - */ - public static fetchCompleteLocationById(locationId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT loc_id, series_id, user_id, loc_name, loc_original_name, last_update FROM series_locations WHERE loc_id = ?'; - const locations: SeriesLocationsTableResult[] = db.all(query, [locationId]) as SeriesLocationsTableResult[]; - return locations; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le lieu complet.` : `Unable to retrieve complete location.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete location element by ID for sync. - */ - public static fetchCompleteLocationElementById(elementId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationElementsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT element_id, location_id, user_id, element_name, original_name, element_description, last_update FROM series_location_elements WHERE element_id = ?'; - const elements: SeriesLocationElementsTableResult[] = db.all(query, [elementId]) as SeriesLocationElementsTableResult[]; - return elements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer l'élément de lieu complet.` : `Unable to retrieve complete location element.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete location sub-element by ID for sync. - */ - public static fetchCompleteLocationSubElementById(subElementId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationSubElementsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update FROM series_location_sub_elements WHERE sub_element_id = ?'; - const subElements: SeriesLocationSubElementsTableResult[] = db.all(query, [subElementId]) as SeriesLocationSubElementsTableResult[]; - return subElements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le sous-élément complet.` : `Unable to retrieve complete sub-element.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a location exists. - */ - public static isLocationExist(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM series_locations WHERE loc_id = ? AND user_id = ?'; - const result: QueryResult | null = db.get(query, [locationId, userId]); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du lieu.` : `Unable to check location existence.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a location element exists. - */ - public static isLocationElementExist(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM series_location_elements WHERE element_id = ? AND user_id = ?'; - const result: QueryResult | null = db.get(query, [elementId, userId]); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'élément.` : `Unable to check element existence.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a location sub-element exists. - */ - public static isLocationSubElementExist(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM series_location_sub_elements WHERE sub_element_id = ? AND user_id = ?'; - const result: QueryResult | null = db.get(query, [subElementId, userId]); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du sous-élément.` : `Unable to check sub-element existence.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series location for sync. - */ - public static insertSyncLocation(locationId: string, seriesId: string, userId: string, locName: string, locOriginalName: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_locations (loc_id, series_id, user_id, loc_name, loc_original_name, last_update) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(loc_id) DO UPDATE SET loc_name = excluded.loc_name, loc_original_name = excluded.loc_original_name, last_update = excluded.last_update'; - const params: SQLiteValue[] = [locationId, seriesId, userId, locName, locOriginalName, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le lieu pour sync.` : `Unable to insert location for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a series location for sync. - */ - public static updateSyncLocation(userId: string, locationId: string, locName: string, locOriginalName: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_locations SET loc_name = ?, loc_original_name = ?, last_update = ? WHERE loc_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [locName, locOriginalName, lastUpdate, locationId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le lieu pour sync.` : `Unable to update location for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series location element for sync. - */ - public static insertSyncLocationElement(elementId: string, locationId: string, userId: string, elementName: string, originalName: string, elementDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_location_elements (element_id, location_id, user_id, element_name, original_name, element_description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(element_id) DO UPDATE SET element_name = excluded.element_name, original_name = excluded.original_name, element_description = excluded.element_description, last_update = excluded.last_update'; - const params: SQLiteValue[] = [elementId, locationId, userId, elementName, originalName, elementDescription, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer l'élément de lieu pour sync.` : `Unable to insert location element for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a series location element for sync. - */ - public static updateSyncLocationElement(userId: string, elementId: string, elementName: string, originalName: string, elementDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_location_elements SET element_name = ?, original_name = ?, element_description = ?, last_update = ? WHERE element_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [elementName, originalName, elementDescription, lastUpdate, elementId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'élément de lieu pour sync.` : `Unable to update location element for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series location sub-element for sync. - */ - public static insertSyncLocationSubElement(subElementId: string, elementId: string, userId: string, subElemName: string, originalName: string, subElemDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_location_sub_elements (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(sub_element_id) DO UPDATE SET sub_elem_name = excluded.sub_elem_name, original_name = excluded.original_name, sub_elem_description = excluded.sub_elem_description, last_update = excluded.last_update'; - const params: SQLiteValue[] = [subElementId, elementId, userId, subElemName, originalName, subElemDescription, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le sous-élément pour sync.` : `Unable to insert sub-element for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a series location sub-element for sync. - */ - public static updateSyncLocationSubElement(userId: string, subElementId: string, subElemName: string, originalName: string, subElemDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_location_sub_elements SET sub_elem_name = ?, original_name = ?, sub_elem_description = ?, last_update = ? WHERE sub_element_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [subElemName, originalName, subElemDescription, lastUpdate, subElementId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sous-élément pour sync.` : `Unable to update sub-element for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - public static fetchLocationsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT loc_id, series_id, user_id, loc_name, loc_original_name, last_update FROM series_locations WHERE series_id = ?'; - return db.all(query, [seriesId]) as SeriesLocationsTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les lieux pour sync.` : `Unable to retrieve locations for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - public static fetchLocationElementsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationElementsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT sle.element_id, sle.location_id, sle.user_id, sle.element_name, sle.original_name, sle.element_description, sle.last_update FROM series_location_elements sle INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ?'; - return db.all(query, [seriesId]) as SeriesLocationElementsTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de lieu pour sync.` : `Unable to retrieve location elements for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - public static fetchLocationSubElementsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationSubElementsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT slse.sub_element_id, slse.element_id, slse.user_id, slse.sub_elem_name, slse.original_name, slse.sub_elem_description, slse.last_update FROM series_location_sub_elements slse INNER JOIN series_location_elements sle ON slse.element_id = sle.element_id INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ?'; - return db.all(query, [seriesId]) as SeriesLocationSubElementsTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments de lieu pour sync.` : `Unable to retrieve location sub-elements for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all locations for a series (alias for fetchSeriesLocationsTable). - */ - public static fetchSeriesLocations(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationsTableResult[] { - return this.fetchSeriesLocationsTable(userId, seriesId, lang); - } - - /** - * Fetches all location elements for a series by series ID. - */ - public static fetchSeriesLocationElementsBySeriesId(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationElementsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT sle.element_id, sle.location_id, sle.user_id, sle.element_name, sle.original_name, sle.element_description, sle.last_update FROM series_location_elements sle INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ? AND sl.user_id = ?'; - return db.all(query, [seriesId, userId]) as SeriesLocationElementsTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de lieu par série.` : `Unable to retrieve location elements by series.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all location sub-elements for a series by series ID. - */ - public static fetchSeriesLocationSubElementsBySeriesId(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesLocationSubElementsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT slse.sub_element_id, slse.element_id, slse.user_id, slse.sub_elem_name, slse.original_name, slse.sub_elem_description, slse.last_update FROM series_location_sub_elements slse INNER JOIN series_location_elements sle ON slse.element_id = sle.element_id INNER JOIN series_locations sl ON sle.location_id = sl.loc_id WHERE sl.series_id = ? AND sl.user_id = ?'; - return db.all(query, [seriesId, userId]) as SeriesLocationSubElementsTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les sous-éléments de lieu par série.` : `Unable to retrieve location sub-elements by series.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a series location exists (alias for isLocationExist). - */ - public static seriesLocationExists(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): boolean { - return this.isLocationExist(userId, locationId, lang); - } - - /** - * Checks if a series location element exists (alias for isLocationElementExist). - */ - public static seriesLocationElementExists(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { - return this.isLocationElementExist(userId, elementId, lang); - } - - /** - * Checks if a series location sub-element exists (alias for isLocationSubElementExist). - */ - public static seriesLocationSubElementExists(userId: string, subElementId: string, lang: 'fr' | 'en' = 'fr'): boolean { - return this.isLocationSubElementExist(userId, subElementId, lang); - } - - /** - * Inserts a series location for sync (alias with compatible signature). - */ - public static insertSyncSeriesLocation(locationId: string, seriesId: string, userId: string, locName: string, locOriginalName: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - return this.insertSyncLocation(locationId, seriesId, userId, locName, locOriginalName, lastUpdate, lang); - } - - /** - * Updates a series location for sync (without originalName). - */ - public static updateSyncSeriesLocation(locationId: string, userId: string, locName: string, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_locations SET loc_name = ?, last_update = ? WHERE loc_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [locName, lastUpdate, locationId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le lieu série pour sync.` : `Unable to update series location for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series location element for sync (alias with compatible signature). - */ - public static insertSyncSeriesLocationElement(elementId: string, locationId: string, userId: string, elementName: string, originalName: string, elementDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - return this.insertSyncLocationElement(elementId, locationId, userId, elementName, originalName, elementDescription, lastUpdate, lang); - } - - /** - * Updates a series location element for sync (without originalName). - */ - public static updateSyncSeriesLocationElement(elementId: string, userId: string, elementName: string, elementDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_location_elements SET element_name = ?, element_description = ?, last_update = ? WHERE element_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [elementName, elementDescription, lastUpdate, elementId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'élément de lieu série pour sync.` : `Unable to update series location element for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series location sub-element for sync (alias with compatible signature). - */ - public static insertSyncSeriesLocationSubElement(subElementId: string, elementId: string, userId: string, subElemName: string, originalName: string, subElemDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - return this.insertSyncLocationSubElement(subElementId, elementId, userId, subElemName, originalName, subElemDescription, lastUpdate, lang); - } - - /** - * Updates a series location sub-element for sync (without originalName). - */ - public static updateSyncSeriesLocationSubElement(subElementId: string, userId: string, subElemName: string, subElemDescription: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_location_sub_elements SET sub_elem_name = ?, sub_elem_description = ?, last_update = ? WHERE sub_element_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [subElemName, subElemDescription, lastUpdate, subElementId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sous-élément série pour sync.` : `Unable to update series sub-element for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } -} diff --git a/electron/database/repositories/series-spell.repo.ts b/electron/database/repositories/series-spell.repo.ts deleted file mode 100644 index 250b685..0000000 --- a/electron/database/repositories/series-spell.repo.ts +++ /dev/null @@ -1,647 +0,0 @@ -import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; -import System from "../System.js"; - -export interface SeriesSpellResult extends Record { - spell_id: string; - series_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 SeriesSpellTagResult extends Record { - tag_id: string; - name: string; - color: string | null; -} - -export interface SeriesSpellsTableResult extends Record { - spell_id: string; - series_id: string; - user_id: string; - name: string; - name_hash: string; - description: string | null; - appearance: string | null; - tags: string | null; - power_level: string | null; - components: string | null; - limitations: string | null; - notes: string | null; - last_update: number; -} - -export interface SeriesSpellTagsTableResult extends Record { - tag_id: string; - series_id: string; - user_id: string; - name: string; - hashed_name: string; - color: string | null; - last_update: number; -} - -export interface SyncedSeriesSpellResult extends Record { - spell_id: string; - series_id: string; - name: string; - last_update: number; -} - -export interface SyncedSeriesSpellTagResult extends Record { - tag_id: string; - series_id: string; - name: string; - last_update: number; -} - -export default class SeriesSpellRepo { - /** - * Fetches all spells for a specific series. - */ - static fetchSpells(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT spell_id, series_id, name, description, appearance, tags, power_level, components, limitations, notes FROM series_spells WHERE user_id=? AND series_id=?'; - const spells: SeriesSpellResult[] = db.all(query, [userId, seriesId]) as SeriesSpellResult[]; - return spells; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts.` : `Unable to retrieve spells.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a single spell by its ID. - */ - static fetchSpellById(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellResult | null { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT spell_id, series_id, name, description, appearance, tags, power_level, components, limitations, notes FROM series_spells WHERE user_id=? AND spell_id=?'; - const spell: SeriesSpellResult | undefined = db.get(query, [userId, spellId]) as SeriesSpellResult | undefined; - return spell || null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le sort.` : `Unable to retrieve spell.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new spell. - */ - static insertSpell(spellId: string, seriesId: 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 { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_spells (spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [spellId, seriesId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter le sort.` : `Unable to add spell.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du sort.` : `Error adding spell.`); - } - return spellId; - } - - /** - * Updates an existing spell. - */ - 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 series_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 updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort.` : `Unable to update spell.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Deletes a spell. - */ - static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM series_spells WHERE spell_id=? AND user_id=?'; - const deleteResult: RunResult = db.run(query, [spellId, userId]); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer le sort.` : `Unable to delete spell.`); - } else { - console.error("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 series. - */ - static fetchTags(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellTagResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT tag_id, name, color FROM series_spell_tags WHERE user_id=? AND series_id=?'; - const tags: SeriesSpellTagResult[] = db.all(query, [userId, seriesId]) as SeriesSpellTagResult[]; - return tags; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les tags.` : `Unable to retrieve tags.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new spell tag. - */ - static insertTag(tagId: string, seriesId: string, userId: string, name: string, hashedName: string, color: string | null, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_spell_tags (tag_id, series_id, user_id, name, hashed_name, color, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [tagId, seriesId, userId, name, hashedName, color, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter le tag.` : `Unable to add tag.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du tag.` : `Error adding tag.`); - } - return tagId; - } - - /** - * Updates an existing spell tag. - */ - static updateTag(userId: string, tagId: string, name: string, hashedName: string, color: string | null, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_spell_tags SET name=?, hashed_name=?, color=?, last_update=? WHERE tag_id=? AND user_id=?'; - const params: SQLiteValue[] = [name, hashedName, color, System.timeStampInSeconds(), tagId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le tag.` : `Unable to update tag.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Deletes a spell tag. - */ - static deleteTag(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM series_spell_tags WHERE tag_id=? AND user_id=?'; - const deleteResult: RunResult = db.run(query, [tagId, userId]); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer le tag.` : `Unable to delete tag.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a 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 series_spells WHERE spell_id=? AND user_id=?'; - const result: QueryResult | null = db.get(query, [spellId, userId]); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du sort.` : `Unable to check spell existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all spells for a series for sync. - */ - static fetchSeriesSpellsTable(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE series_id = ? AND user_id = ?'; - const spells: SeriesSpellsTableResult[] = db.all(query, [seriesId, userId]) as SeriesSpellsTableResult[]; - return spells; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts pour sync.` : `Unable to retrieve spells for sync.`); - } else { - console.error("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 series for sync. - */ - static fetchSeriesSpellTagsTable(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellTagsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE series_id = ? AND user_id = ?'; - const tags: SeriesSpellTagsTableResult[] = db.all(query, [seriesId, userId]) as SeriesSpellTagsTableResult[]; - return tags; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sort pour sync.` : `Unable to retrieve spell tags for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all series spells for a user for sync comparison. - */ - static fetchSyncedSeriesSpells(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesSpellResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT spell_id, series_id, name, last_update FROM series_spells WHERE user_id = ?'; - const spells: SyncedSeriesSpellResult[] = db.all(query, [userId]) as SyncedSeriesSpellResult[]; - return spells; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts de série pour sync.` : `Unable to retrieve series spells for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all series spell tags for a user for sync comparison. - */ - static fetchSyncedSeriesSpellTags(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesSpellTagResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT tag_id, series_id, name, last_update FROM series_spell_tags WHERE user_id = ?'; - const tags: SyncedSeriesSpellTagResult[] = db.all(query, [userId]) as SyncedSeriesSpellTagResult[]; - return tags; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sort pour sync.` : `Unable to retrieve spell tags for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete spell by ID for sync. - */ - static fetchSpellTableById(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellsTableResult | null { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE spell_id = ? AND user_id = ?'; - const spell: SeriesSpellsTableResult | undefined = db.get(query, [spellId, userId]) as SeriesSpellsTableResult | undefined; - return spell || null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le sort complet.` : `Unable to retrieve complete spell.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete spell tag by ID for sync. - */ - static fetchSpellTagTableById(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellTagsTableResult | null { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE tag_id = ? AND user_id = ?'; - const tag: SeriesSpellTagsTableResult | undefined = db.get(query, [tagId, userId]) as SeriesSpellTagsTableResult | undefined; - return tag || null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le tag complet.` : `Unable to retrieve complete tag.`); - } else { - console.error("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. - */ - static isSpellTagExist(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM series_spell_tags WHERE tag_id=? AND user_id=?'; - const result: QueryResult | null = db.get(query, [tagId, userId]); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`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("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series spell for sync. - */ - static insertSyncSpell(spellId: string, seriesId: 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 INTO series_spells (spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(spell_id) DO UPDATE SET name = excluded.name, name_hash = excluded.name_hash, description = excluded.description, appearance = excluded.appearance, tags = excluded.tags, power_level = excluded.power_level, components = excluded.components, limitations = excluded.limitations, notes = excluded.notes, last_update = excluded.last_update'; - const params: SQLiteValue[] = [spellId, seriesId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le sort pour sync.` : `Unable to insert spell for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a series spell for sync. - */ - 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 series_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 updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort pour sync.` : `Unable to update spell for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series spell tag for sync. - */ - static insertSyncSpellTag(tagId: string, seriesId: string, userId: string, name: string, hashedName: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_spell_tags (tag_id, series_id, user_id, name, hashed_name, color, last_update) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(tag_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, color = excluded.color, last_update = excluded.last_update'; - const params: SQLiteValue[] = [tagId, seriesId, userId, name, hashedName, color, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le tag pour sync.` : `Unable to insert tag for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a series spell tag for sync. - */ - static updateSyncSpellTag(userId: string, tagId: string, name: string, hashedName: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_spell_tags SET name = ?, hashed_name = ?, color = ?, last_update = ? WHERE tag_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [name, hashedName, color, lastUpdate, tagId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le tag pour sync.` : `Unable to update tag for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - static fetchSpellsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE series_id = ?'; - return db.all(query, [seriesId]) as SeriesSpellsTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts pour sync.` : `Unable to retrieve spells for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - static fetchSpellTagsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellTagsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE series_id = ?'; - return db.all(query, [seriesId]) as SeriesSpellTagsTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sort pour sync.` : `Unable to retrieve spell tags for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all spells for a series (alias for fetchSeriesSpellsTable). - */ - static fetchSeriesSpells(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellsTableResult[] { - return this.fetchSeriesSpellsTable(userId, seriesId, lang); - } - - /** - * Fetches all spell tags for a series (alias for fetchSeriesSpellTagsTable). - */ - static fetchSeriesSpellTags(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellTagsTableResult[] { - return this.fetchSeriesSpellTagsTable(userId, seriesId, lang); - } - - /** - * Checks if a series spell exists (alias for isSpellExist). - */ - static seriesSpellExists(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean { - return this.isSpellExist(userId, spellId, lang); - } - - /** - * Checks if a series spell tag exists (alias for isSpellTagExist). - */ - static seriesSpellTagExists(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean { - return this.isSpellTagExist(userId, tagId, lang); - } - - /** - * Fetches a complete spell by ID for sync (array format). - */ - static fetchCompleteSpellById(spellId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT spell_id, series_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM series_spells WHERE spell_id = ?'; - return db.all(query, [spellId]) as SeriesSpellsTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le sort complet.` : `Unable to retrieve complete spell.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete spell tag by ID for sync (array format). - */ - static fetchCompleteSpellTagById(tagId: string, lang: 'fr' | 'en' = 'fr'): SeriesSpellTagsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT tag_id, series_id, user_id, name, hashed_name, color, last_update FROM series_spell_tags WHERE tag_id = ?'; - return db.all(query, [tagId]) as SeriesSpellTagsTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le tag complet.` : `Unable to retrieve complete tag.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series spell for sync (alias with compatible signature). - */ - static insertSyncSeriesSpell(spellId: string, seriesId: 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 { - return this.insertSyncSpell(spellId, seriesId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate, lang); - } - - /** - * Updates a series spell for sync (simplified signature). - */ - static updateSyncSeriesSpell(spellId: string, userId: string, name: 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 series_spells SET name = ?, description = ?, appearance = ?, tags = ?, power_level = ?, components = ?, limitations = ?, notes = ?, last_update = ? WHERE spell_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [name, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate, spellId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort série pour sync.` : `Unable to update series spell for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series spell tag for sync (alias with compatible signature). - */ - static insertSyncSeriesSpellTag(tagId: string, seriesId: string, userId: string, name: string, hashedName: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - return this.insertSyncSpellTag(tagId, seriesId, userId, name, hashedName, color, lastUpdate, lang); - } - - /** - * Updates a series spell tag for sync (simplified signature). - */ - static updateSyncSeriesSpellTag(tagId: string, userId: string, name: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_spell_tags SET name = ?, color = ?, last_update = ? WHERE tag_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [name, color, lastUpdate, tagId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le tag série pour sync.` : `Unable to update series tag for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } -} diff --git a/electron/database/repositories/series-sync.repo.ts b/electron/database/repositories/series-sync.repo.ts deleted file mode 100644 index fb401b6..0000000 --- a/electron/database/repositories/series-sync.repo.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; -import System from "../System.js"; - -export type SyncElementType = 'character' | 'world' | 'location' | 'spell'; - -export interface BookElementSeriesLink extends Record { - series_id: string | null; -} - -export default class SeriesSyncRepo { - /** - * Gets the series element ID linked to a book character. - */ - static getCharacterSeriesLink(userId: string, characterId: string, lang: 'fr' | 'en' = 'fr'): string | null { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT series_character_id AS series_id FROM book_characters WHERE character_id = ? AND user_id = ?'; - const result: BookElementSeriesLink | undefined = db.get(query, [characterId, userId]) as BookElementSeriesLink | undefined; - return result ? result.series_id : null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le lien série du personnage.` : `Unable to retrieve character series link.`); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Gets the series element ID linked to a book world. - */ - static getWorldSeriesLink(userId: string, worldId: string, lang: 'fr' | 'en' = 'fr'): string | null { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT series_world_id AS series_id FROM book_world WHERE world_id = ? AND user_id = ?'; - const result: BookElementSeriesLink | undefined = db.get(query, [worldId, userId]) as BookElementSeriesLink | undefined; - return result ? result.series_id : null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le lien série du monde.` : `Unable to retrieve world series link.`); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Gets the series element ID linked to a book location. - */ - static getLocationSeriesLink(userId: string, locationId: string, lang: 'fr' | 'en' = 'fr'): string | null { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT series_location_id AS series_id FROM book_location WHERE loc_id = ? AND user_id = ?'; - const result: BookElementSeriesLink | undefined = db.get(query, [locationId, userId]) as BookElementSeriesLink | undefined; - return result ? result.series_id : null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le lien série du lieu.` : `Unable to retrieve location series link.`); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Gets the series element ID linked to a book spell. - */ - static getSpellSeriesLink(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): string | null { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT series_spell_id AS series_id FROM book_spells WHERE spell_id = ? AND user_id = ?'; - const result: BookElementSeriesLink | undefined = db.get(query, [spellId, userId]) as BookElementSeriesLink | undefined; - return result ? result.series_id : null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le lien série du sort.` : `Unable to retrieve spell series link.`); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Updates a field in series_characters table. - */ - static updateSeriesCharacterField(userId: string, seriesCharacterId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): boolean { - const allowedFields: string[] = ['first_name', 'last_name', 'nickname', 'age', 'gender', 'species', 'nationality', 'status', 'title', 'category', 'role', 'biography', 'history', 'speech_pattern', 'catchphrase', 'residence', 'notes', 'color']; - if (!allowedFields.includes(field)) { - throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); - } - try { - const db: Database = System.getDb(); - const query: string = `UPDATE series_characters SET ${field} = ?, last_update = ? WHERE character_id = ? AND user_id = ?`; - const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesCharacterId, 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 mettre à jour le personnage série.` : `Unable to update series character.`); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Updates a field in all book_characters linked to a series character. - */ - static updateLinkedBookCharactersField(userId: string, seriesCharacterId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): number { - const allowedFields: string[] = ['first_name', 'last_name', 'nickname', 'age', 'gender', 'species', 'nationality', 'status', 'title', 'category', 'role', 'biography', 'history', 'speech_pattern', 'catchphrase', 'residence', 'notes', 'color']; - if (!allowedFields.includes(field)) { - throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); - } - try { - const db: Database = System.getDb(); - const query: string = `UPDATE book_characters SET ${field} = ?, last_update = ? WHERE series_character_id = ? AND user_id = ?`; - const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesCharacterId, userId]); - return result.changes; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour les personnages liés.` : `Unable to update linked characters.`); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Updates a field in series_worlds table. - */ - static updateSeriesWorldField(userId: string, seriesWorldId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): boolean { - const allowedFields: string[] = ['name', 'history', 'politics', 'economy', 'religion', 'languages']; - if (!allowedFields.includes(field)) { - throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); - } - try { - const db: Database = System.getDb(); - const query: string = `UPDATE series_worlds SET ${field} = ?, last_update = ? WHERE world_id = ? AND user_id = ?`; - const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesWorldId, 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 mettre à jour le monde série.` : `Unable to update series world.`); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Updates a field in all book_world linked to a series world. - */ - static updateLinkedBookWorldsField(userId: string, seriesWorldId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): number { - const allowedFields: string[] = ['name', 'history', 'politics', 'economy', 'religion', 'languages']; - if (!allowedFields.includes(field)) { - throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); - } - try { - const db: Database = System.getDb(); - const query: string = `UPDATE book_world SET ${field} = ?, last_update = ? WHERE series_world_id = ? AND user_id = ?`; - const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesWorldId, userId]); - return result.changes; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour les mondes liés.` : `Unable to update linked worlds.`); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Updates a field in series_locations table. - */ - static updateSeriesLocationField(userId: string, seriesLocationId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): boolean { - const allowedFields: string[] = ['name']; - if (!allowedFields.includes(field)) { - throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); - } - try { - const db: Database = System.getDb(); - const query: string = `UPDATE series_locations SET ${field} = ?, last_update = ? WHERE location_id = ? AND user_id = ?`; - const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesLocationId, 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 mettre à jour le lieu série.` : `Unable to update series location.`); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Updates a field in all book_location linked to a series location. - */ - static updateLinkedBookLocationsField(userId: string, seriesLocationId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): number { - const allowedFields: string[] = ['loc_name']; - if (!allowedFields.includes(field)) { - throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); - } - try { - const db: Database = System.getDb(); - const query: string = `UPDATE book_location SET ${field} = ?, last_update = ? WHERE series_location_id = ? AND user_id = ?`; - const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesLocationId, userId]); - return result.changes; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour les lieux liés.` : `Unable to update linked locations.`); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Updates a field in series_spells table. - */ - static updateSeriesSpellField(userId: string, seriesSpellId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): boolean { - const allowedFields: string[] = ['name', 'description', 'type', 'level', 'range', 'duration', 'cost', 'effect', 'components', 'notes']; - if (!allowedFields.includes(field)) { - throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); - } - try { - const db: Database = System.getDb(); - const query: string = `UPDATE series_spells SET ${field} = ?, last_update = ? WHERE spell_id = ? AND user_id = ?`; - const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesSpellId, 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 mettre à jour le sort série.` : `Unable to update series spell.`); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - - /** - * Updates a field in all book_spells linked to a series spell. - */ - static updateLinkedBookSpellsField(userId: string, seriesSpellId: string, field: string, encryptedValue: string, lang: 'fr' | 'en' = 'fr'): number { - const allowedFields: string[] = ['name', 'description', 'type', 'level', 'range', 'duration', 'cost', 'effect', 'components', 'notes']; - if (!allowedFields.includes(field)) { - throw new Error(lang === 'fr' ? `Champ non autorisé: ${field}` : `Field not allowed: ${field}`); - } - try { - const db: Database = System.getDb(); - const query: string = `UPDATE book_spells SET ${field} = ?, last_update = ? WHERE series_spell_id = ? AND user_id = ?`; - const result: RunResult = db.run(query, [encryptedValue, System.timeStampInSeconds(), seriesSpellId, userId]); - return result.changes; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour les sorts liés.` : `Unable to update linked spells.`); - } - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } -} diff --git a/electron/database/repositories/series-world.repo.ts b/electron/database/repositories/series-world.repo.ts deleted file mode 100644 index 4622f77..0000000 --- a/electron/database/repositories/series-world.repo.ts +++ /dev/null @@ -1,555 +0,0 @@ -import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; -import System from "../System.js"; - -export interface SeriesWorldResult extends Record { - world_id: string; - world_name: string; - history: string; - politics: string; - economy: string; - religion: string; - languages: string; - element_id: string; - element_name: string; - element_description: string; - element_type: number; -} - -export interface SeriesWorldsTableResult extends Record { - 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 SeriesWorldElementsTableResult extends Record { - 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 SyncedSeriesWorldResult extends Record { - world_id: string; - series_id: string; - name: string; - last_update: number; -} - -export interface SyncedSeriesWorldElementResult extends Record { - element_id: string; - world_id: string; - name: string; - last_update: number; -} - -export default class SeriesWorldRepo { - /** - * Checks if a world with the given hashed name already exists for a user and series. - */ - public static checkWorldExist(userId: string, seriesId: string, worldName: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT world_id FROM series_worlds WHERE user_id=? AND series_id=? AND hashed_name=?'; - const result: QueryResult | null = db.get(query, [userId, seriesId, worldName]); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du monde.` : `Unable to verify world existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new world into the series. - */ - public static insertNewWorld(worldId: string, userId: string, seriesId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_worlds (world_id, user_id, series_id, name, hashed_name, last_update) VALUES (?,?,?,?,?,?)'; - const params: SQLiteValue[] = [worldId, userId, seriesId, encryptedName, hashedName, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter le monde.` : `Unable to add world.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du monde.` : `Error adding world.`); - } - return worldId; - } - - /** - * Fetches all worlds and their elements for a given series. - */ - public static fetchWorlds(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT world.world_id AS world_id, world.name AS world_name, world.history, world.politics, world.economy, world.religion, world.languages, element.element_id AS element_id, element.name AS element_name, element.description AS element_description, element.element_type FROM series_worlds AS world LEFT JOIN series_world_elements AS element ON world.world_id = element.world_id WHERE world.user_id = ? AND world.series_id = ?'; - const worlds: SeriesWorldResult[] = db.all(query, [userId, seriesId]) as SeriesWorldResult[]; - return worlds; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les mondes.` : `Unable to retrieve worlds.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a world's information. - */ - public static updateWorld(userId: string, worldId: string, encryptedName: string, hashedName: string, history: string | null, politics: string | null, economy: string | null, religion: string | null, languages: string | null, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_worlds SET name=?, hashed_name=?, history=?, politics=?, economy=?, religion=?, languages=?, last_update=? WHERE world_id=? AND user_id=?'; - const params: SQLiteValue[] = [encryptedName, hashedName, history, politics, economy, religion, languages, System.timeStampInSeconds(), worldId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le monde.` : `Unable to update world.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new element for a world. - */ - public static insertElement(elementId: string, worldId: string, userId: string, elementType: number, encryptedName: string, originalName: string, description: string | null, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?,?,?,?,?,?,?,?)'; - const params: SQLiteValue[] = [elementId, worldId, userId, elementType, encryptedName, originalName, description, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter l'élément.` : `Unable to add element.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout de l'élément.` : `Error adding element.`); - } - return elementId; - } - - /** - * Deletes an element from a world. - */ - public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM series_world_elements WHERE element_id=? AND user_id=?'; - const deleteResult: RunResult = db.run(query, [elementId, userId]); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer l'élément.` : `Unable to delete element.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all worlds for a series for sync. - */ - public static fetchSeriesWorldsTable(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE series_id = ? AND user_id = ?'; - const worlds: SeriesWorldsTableResult[] = db.all(query, [seriesId, userId]) as SeriesWorldsTableResult[]; - return worlds; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les mondes pour sync.` : `Unable to retrieve worlds for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all elements for a world for sync. - */ - public static fetchSeriesWorldElementsTable(userId: string, worldId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldElementsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM series_world_elements WHERE world_id = ? AND user_id = ?'; - const elements: SeriesWorldElementsTableResult[] = db.all(query, [worldId, userId]) as SeriesWorldElementsTableResult[]; - return elements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de monde pour sync.` : `Unable to retrieve world elements for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all series worlds for a user for sync comparison. - */ - public static fetchSyncedSeriesWorlds(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesWorldResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT world_id, series_id, name, last_update FROM series_worlds WHERE user_id = ?'; - const worlds: SyncedSeriesWorldResult[] = db.all(query, [userId]) as SyncedSeriesWorldResult[]; - return worlds; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les mondes de série pour sync.` : `Unable to retrieve series worlds for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all series world elements for a user for sync comparison. - */ - public static fetchSyncedSeriesWorldElements(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesWorldElementResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT element_id, world_id, name, last_update FROM series_world_elements WHERE user_id = ?'; - const elements: SyncedSeriesWorldElementResult[] = db.all(query, [userId]) as SyncedSeriesWorldElementResult[]; - return elements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de monde pour sync.` : `Unable to retrieve world elements for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete world by ID for sync. - */ - public static fetchCompleteWorldById(worldId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE world_id = ?'; - const worlds: SeriesWorldsTableResult[] = db.all(query, [worldId]) as SeriesWorldsTableResult[]; - return worlds; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer le monde complet.` : `Unable to retrieve complete world.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete world element by ID for sync. - */ - public static fetchCompleteWorldElementById(elementId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldElementsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM series_world_elements WHERE element_id = ?'; - const elements: SeriesWorldElementsTableResult[] = db.all(query, [elementId]) as SeriesWorldElementsTableResult[]; - return elements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer l'élément de monde complet.` : `Unable to retrieve complete world element.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a world exists. - */ - public static isWorldExist(userId: string, worldId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM series_worlds WHERE world_id=? AND user_id=?'; - const result: QueryResult | null = db.get(query, [worldId, userId]); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du monde.` : `Unable to check world existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a world element exists. - */ - public static isWorldElementExist(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM series_world_elements WHERE element_id=? AND user_id=?'; - const result: QueryResult | null = db.get(query, [elementId, userId]); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'élément.` : `Unable to check element existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series world for sync. - */ - public static insertSyncWorld(worldId: string, seriesId: string, userId: string, name: string, hashedName: string, history: string | null, politics: string | null, economy: string | null, religion: string | null, languages: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_worlds (world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(world_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, history = excluded.history, politics = excluded.politics, economy = excluded.economy, religion = excluded.religion, languages = excluded.languages, last_update = excluded.last_update'; - const params: SQLiteValue[] = [worldId, seriesId, userId, name, hashedName, history, politics, economy, religion, languages, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le monde pour sync.` : `Unable to insert world for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a series world for sync. - */ - public static updateSyncWorld(userId: string, worldId: string, name: string, hashedName: string, history: string | null, politics: string | null, economy: string | null, religion: string | null, languages: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_worlds SET name = ?, hashed_name = ?, history = ?, politics = ?, economy = ?, religion = ?, languages = ?, last_update = ? WHERE world_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [name, hashedName, history, politics, economy, religion, languages, lastUpdate, worldId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le monde pour sync.` : `Unable to update world for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series world element for sync. - */ - public static insertSyncWorldElement(elementId: string, worldId: string, userId: string, elementType: number, name: string, originalName: string, description: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(element_id) DO UPDATE SET element_type = excluded.element_type, name = excluded.name, original_name = excluded.original_name, description = excluded.description, last_update = excluded.last_update'; - const params: SQLiteValue[] = [elementId, worldId, userId, elementType, name, originalName, description, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer l'élément de monde pour sync.` : `Unable to insert world element for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a series world element for sync. - */ - public static updateSyncWorldElement(userId: string, elementId: string, elementType: number, name: string, originalName: string, description: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_world_elements SET element_type = ?, name = ?, original_name = ?, description = ?, last_update = ? WHERE element_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [elementType, name, originalName, description, lastUpdate, elementId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'élément de monde pour sync.` : `Unable to update world element for sync.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - public static fetchWorldsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT world_id, series_id, user_id, name, hashed_name, history, politics, economy, religion, languages, last_update FROM series_worlds WHERE series_id = ?'; - return db.all(query, [seriesId]) as SeriesWorldsTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les mondes pour sync.` : `Unable to retrieve worlds for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - public static fetchWorldElementsTableForSync(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldElementsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT swe.element_id, swe.world_id, swe.user_id, swe.element_type, swe.name, swe.original_name, swe.description, swe.last_update FROM series_world_elements swe INNER JOIN series_worlds sw ON swe.world_id = sw.world_id WHERE sw.series_id = ?'; - return db.all(query, [seriesId]) as SeriesWorldElementsTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de monde pour sync.` : `Unable to retrieve world elements for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all worlds for a series (alias for fetchSeriesWorldsTable). - */ - public static fetchSeriesWorlds(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldsTableResult[] { - return this.fetchSeriesWorldsTable(userId, seriesId, lang); - } - - /** - * Fetches all world elements for a series by series ID. - */ - public static fetchSeriesWorldElementsBySeriesId(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldElementsTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT swe.element_id, swe.world_id, swe.user_id, swe.element_type, swe.name, swe.original_name, swe.description, swe.last_update FROM series_world_elements swe INNER JOIN series_worlds sw ON swe.world_id = sw.world_id WHERE sw.series_id = ? AND sw.user_id = ?'; - return db.all(query, [seriesId, userId]) as SeriesWorldElementsTableResult[]; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de monde par série.` : `Unable to retrieve world elements by series.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a series world exists (alias for isWorldExist). - */ - public static seriesWorldExists(userId: string, worldId: string, lang: 'fr' | 'en' = 'fr'): boolean { - return this.isWorldExist(userId, worldId, lang); - } - - /** - * Checks if a series world element exists (alias for isWorldElementExist). - */ - public static seriesWorldElementExists(userId: string, elementId: string, lang: 'fr' | 'en' = 'fr'): boolean { - return this.isWorldElementExist(userId, elementId, lang); - } - - /** - * Inserts a series world for sync (alias with compatible signature). - */ - public static insertSyncSeriesWorld(worldId: string, seriesId: string, userId: string, name: string, hashedName: string, history: string | null, politics: string | null, economy: string | null, religion: string | null, languages: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - return this.insertSyncWorld(worldId, seriesId, userId, name, hashedName, history, politics, economy, religion, languages, lastUpdate, lang); - } - - /** - * Updates a series world for sync (without hashedName). - */ - public static updateSyncSeriesWorld(worldId: string, userId: string, name: string, history: string | null, politics: string | null, economy: string | null, religion: string | null, languages: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_worlds SET name = ?, history = ?, politics = ?, economy = ?, religion = ?, languages = ?, last_update = ? WHERE world_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [name, history, politics, economy, religion, languages, lastUpdate, worldId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le monde série pour sync.` : `Unable to update series world for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series world element for sync (alias with compatible signature). - */ - public static insertSyncSeriesWorldElement(elementId: string, worldId: string, userId: string, elementType: number, name: string, originalName: string, description: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - return this.insertSyncWorldElement(elementId, worldId, userId, elementType, name, originalName, description, lastUpdate, lang); - } - - /** - * Updates a series world element for sync (without elementType and originalName). - */ - public static updateSyncSeriesWorldElement(elementId: string, userId: string, name: string, description: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE series_world_elements SET name = ?, description = ?, last_update = ? WHERE element_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [name, description, lastUpdate, elementId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'élément de monde série pour sync.` : `Unable to update series world element for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } -} diff --git a/electron/database/repositories/series.repo.ts b/electron/database/repositories/series.repo.ts deleted file mode 100644 index 67595c9..0000000 --- a/electron/database/repositories/series.repo.ts +++ /dev/null @@ -1,553 +0,0 @@ -import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; -import System from "../System.js"; - -export interface SeriesResult extends Record { - series_id: string; - user_id: string; - name: string; - hashed_name: string; - description: string | null; - cover_image: string | null; - last_update: number; -} - -export interface SeriesBookResult extends Record { - series_id: string; - book_id: string; - book_order: number; - title: string; - cover_image: string | null; -} - -export interface SeriesListItem extends Record { - series_id: string; - name: string; - description: string | null; - cover_image: string | null; - book_count: number; - book_ids: string | null; -} - -export interface SeriesTableResult extends Record { - series_id: string; - user_id: string; - name: string; - hashed_name: string; - description: string | null; - cover_image: string | null; - last_update: number; -} - -export interface SeriesBooksTableResult extends Record { - series_id: string; - book_id: string; - book_order: number; - last_update: number; -} - -export interface SyncedSeriesResult extends Record { - series_id: string; - name: string; - description: string | null; - last_update: number; -} - -export interface SyncedSeriesBookResult extends Record { - series_id: string; - book_id: string; - book_order: number; - last_update: number; -} - -export default class SeriesRepo { - /** - * Fetches all series 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 series with book counts - */ - public static fetchUserSeries(userId: string, lang: 'fr' | 'en' = 'fr'): SeriesListItem[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT series.series_id, series.name, series.description, series.cover_image, COUNT(series_books.book_id) AS book_count, GROUP_CONCAT(series_books.book_id) AS book_ids FROM book_series series LEFT JOIN series_books ON series.series_id = series_books.series_id WHERE series.user_id = ? GROUP BY series.series_id, series.last_update ORDER BY series.last_update DESC'; - const series: SeriesListItem[] = db.all(query, [userId]) as SeriesListItem[]; - return series; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les séries.` : `Unable to retrieve series.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a single series by its ID. - * @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 result or null if not found - */ - public static fetchSeriesById(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesResult | null { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ? AND user_id = ?'; - const series: SeriesResult | undefined = db.get(query, [seriesId, userId]) as SeriesResult | undefined; - return series || null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer la série.` : `Unable to retrieve series.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new series. - * @param seriesId - The unique identifier for the new series - * @param userId - The unique identifier of the user - * @param name - The encrypted name - * @param hashedName - The hashed name for duplicate detection - * @param description - The encrypted description (nullable) - * @param lang - The language for error messages ('fr' or 'en') - * @returns The series ID if successful - */ - public static insertSeries(seriesId: string, userId: string, name: string, hashedName: string, description: string | null, lang: 'fr' | 'en' = 'fr'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_series (series_id, user_id, name, hashed_name, description, last_update) VALUES (?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [seriesId, userId, name, hashedName, description, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de créer la série.` : `Unable to create series.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de la création de la série.` : `Error creating series.`); - } - 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 encrypted name - * @param hashedName - The hashed name - * @param description - The encrypted description (nullable) - * @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, hashedName: string, description: string | null, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE book_series SET name = ?, hashed_name = ?, description = ?, last_update = ? WHERE series_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [name, hashedName, description, System.timeStampInSeconds(), seriesId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour la série.` : `Unable to update series.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * 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 { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM book_series WHERE series_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [seriesId, userId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer la série.` : `Unable to delete series.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all books in a series with their order. - * @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 An array of books in the series - */ - public static fetchSeriesBooks(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesBookResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT sb.series_id, sb.book_id, sb.book_order, b.title, b.cover_image FROM series_books sb INNER JOIN erit_books b ON sb.book_id = b.book_id WHERE sb.series_id = ? AND b.author_id = ? ORDER BY sb.book_order'; - const books: SeriesBookResult[] = db.all(query, [seriesId, userId]) as SeriesBookResult[]; - return books; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les livres de la série.` : `Unable to retrieve series books.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Adds a book to a series. - * @param seriesId - The unique identifier of the series - * @param bookId - The unique identifier of the book - * @param bookOrder - 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(seriesId: string, bookId: string, bookOrder: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_books (series_id, book_id, book_order, last_update) VALUES (?, ?, ?, ?) ON CONFLICT(series_id, book_id) DO UPDATE SET book_order = excluded.book_order, last_update = excluded.last_update'; - const params: SQLiteValue[] = [seriesId, bookId, bookOrder, System.timeStampInSeconds()]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter le livre à la série.` : `Unable to add book to series.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Removes a book from a series. - * @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(seriesId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM series_books WHERE series_id = ? AND book_id = ?'; - const params: SQLiteValue[] = [seriesId, bookId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de retirer le livre de la série.` : `Unable to remove book from series.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates the order of books in a series. - * @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(seriesId: string, booksOrder: {bookId: string, order: number}[], lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const timestamp: number = System.timeStampInSeconds(); - for (const bookOrder of booksOrder) { - const query: string = 'UPDATE series_books SET book_order = ?, last_update = ? WHERE series_id = ? AND book_id = ?'; - db.run(query, [bookOrder.order, timestamp, seriesId, bookOrder.bookId]); - } - return true; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de réordonner les livres.` : `Unable to reorder books.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a series exists for 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 True if the series exists - */ - public static isSeriesExist(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM book_series WHERE series_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [seriesId, userId]; - const result: QueryResult | null = db.get(query, params); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de la série.` : `Unable to check series existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Gets the series ID for a book if it belongs to one. - * @param bookId - The unique identifier of the book - * @param lang - The language for error messages ('fr' or 'en') - * @returns The series ID or null - */ - public static getSeriesIdForBook(bookId: string, lang: 'fr' | 'en' = 'fr'): string | null { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT series_id FROM series_books WHERE book_id = ?'; - const result = db.get(query, [bookId]) as { series_id: string } | undefined; - return result ? result.series_id : null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier la série du livre.` : `Unable to check book series.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a series table row for sync purposes. - * @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 An array containing the series table row - */ - public static fetchSeriesTableForSync(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ? AND user_id = ?'; - const series: SeriesTableResult[] = db.all(query, [seriesId, userId]) as SeriesTableResult[]; - return series; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer la série pour sync.` : `Unable to retrieve series for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all series-books relationships for sync. - * @param seriesId - The unique identifier of the series - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of series-books table rows - */ - public static fetchSeriesBooksTable(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesBooksTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT series_id, book_id, book_order, last_update FROM series_books WHERE series_id = ? ORDER BY book_order'; - const books: SeriesBooksTableResult[] = db.all(query, [seriesId]) as SeriesBooksTableResult[]; - return books; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les livres de la série pour sync.` : `Unable to retrieve series books for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all series for a user for sync comparison. - * @param userId - The unique identifier of the user - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of synced series results - */ - public static fetchSyncedSeries(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT series_id, name, description, last_update FROM book_series WHERE user_id = ? ORDER BY last_update DESC'; - const series: SyncedSeriesResult[] = db.all(query, [userId]) as SyncedSeriesResult[]; - return series; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les séries pour sync.` : `Unable to retrieve series for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all series-books relationships for a user for sync comparison. - * @param userId - The unique identifier of the user - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of synced series book results - */ - public static fetchSyncedSeriesBooks(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSeriesBookResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT sb.series_id, sb.book_id, sb.book_order, sb.last_update FROM series_books sb INNER JOIN book_series bs ON sb.series_id = bs.series_id WHERE bs.user_id = ? ORDER BY sb.book_order'; - const books: SyncedSeriesBookResult[] = db.all(query, [userId]) as SyncedSeriesBookResult[]; - return books; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les livres de séries pour sync.` : `Unable to retrieve series books for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete series by ID for sync. - * @param seriesId - The unique identifier of the series - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array containing the series - */ - public static fetchCompleteSeriesById(seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesTableResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ?'; - const series: SeriesTableResult[] = db.all(query, [seriesId]) as SeriesTableResult[]; - return series; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer la série complète.` : `Unable to retrieve complete series.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series for sync purposes. - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the insertion was successful - */ - public static insertSyncSeries(seriesId: string, userId: string, name: string, hashedName: string, description: string | null, coverImage: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_series (series_id, user_id, name, hashed_name, description, cover_image, last_update) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(series_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, description = excluded.description, cover_image = excluded.cover_image, last_update = excluded.last_update'; - const params: SQLiteValue[] = [seriesId, userId, name, hashedName, description, coverImage, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer la série pour sync.` : `Unable to insert series for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a series for sync purposes. - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the update was successful - */ - public static updateSyncSeries(userId: string, seriesId: string, name: string, hashedName: string, description: string | null, coverImage: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE book_series SET name = ?, hashed_name = ?, description = ?, cover_image = ?, last_update = ? WHERE series_id = ? AND user_id = ?'; - const params: SQLiteValue[] = [name, hashedName, description, coverImage, lastUpdate, seriesId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour la série pour sync.` : `Unable to update series for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a series-book relationship for sync. - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the insertion was successful - */ - public static insertSyncSeriesBook(seriesId: string, bookId: string, bookOrder: number, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO series_books (series_id, book_id, book_order, last_update) VALUES (?, ?, ?, ?) ON CONFLICT(series_id, book_id) DO UPDATE SET book_order = excluded.book_order, last_update = excluded.last_update'; - const params: SQLiteValue[] = [seriesId, bookId, bookOrder, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer la liaison série-livre pour sync.` : `Unable to insert series-book for sync.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a series-book relationship exists. - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the relationship exists - */ - public static isSeriesBookExist(seriesId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM series_books WHERE series_id = ? AND book_id = ?'; - const result: QueryResult | null = db.get(query, [seriesId, bookId]); - return result !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier la liaison série-livre.` : `Unable to check series-book.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a series exists for a user (alias for isSeriesExist). - * @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 series exists - */ - public static seriesExists(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): boolean { - return this.isSeriesExist(userId, seriesId, lang); - } -} diff --git a/electron/database/repositories/spell.repo.ts b/electron/database/repositories/spell.repo.ts deleted file mode 100644 index 71127f2..0000000 --- a/electron/database/repositories/spell.repo.ts +++ /dev/null @@ -1,378 +0,0 @@ -import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; -import System from '../System.js'; - -export interface SpellResult extends Record { - spell_id: string; - book_id: string; - name: string; - description: string | null; - appearance: string | null; - tags: string | null; - power_level: string | null; - components: string | null; - limitations: string | null; - notes: string | null; - series_spell_id: string | null; -} - -export interface BookSpellsTable extends Record { - spell_id: string; - book_id: string; - user_id: string; - name: string; - name_hash: string; - description: string | null; - appearance: string | null; - tags: string | null; - power_level: string | null; - components: string | null; - limitations: string | null; - notes: string | null; - last_update: number; -} - -export interface SyncedSpellResult extends Record { - 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, series_spell_id 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, series_spell_id 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 | null, appearance: string | null, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr', seriesSpellId: string | null = null): string { - let result: RunResult; - try { - const db: Database = System.getDb(); - const query: string = seriesSpellId - ? 'INSERT INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, series_spell_id, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)' - : '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[] = seriesSpellId - ? [spellId, bookId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, seriesSpellId, System.timeStampInSeconds()] - : [spellId, bookId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds()]; - result = db.run(query, params); - } 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.`); - } - 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; - } - - /** - * 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 | null, appearance: string | null, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr', seriesSpellId: string | null = null): boolean { - try { - const db: Database = System.getDb(); - const query: string = seriesSpellId !== null - ? 'UPDATE book_spells SET name=?, name_hash=?, description=?, appearance=?, tags=?, power_level=?, components=?, limitations=?, notes=?, series_spell_id=?, last_update=? WHERE spell_id=? AND user_id=?' - : '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[] = seriesSpellId !== null - ? [name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, seriesSpellId, System.timeStampInSeconds(), spellId, userId] - : [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 | null, appearance: string | null, tags: string | null, 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 | null, appearance: string | null, tags: string | null, 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.`); - } - } -} diff --git a/electron/database/repositories/spelltag.repo.ts b/electron/database/repositories/spelltag.repo.ts deleted file mode 100644 index da6be39..0000000 --- a/electron/database/repositories/spelltag.repo.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; -import System from '../System.js'; - -export interface SpellTagResult extends Record { - tag_id: string; - book_id: string; - name: string; - color: string | null; -} - -export interface BookSpellTagsTable extends Record { - 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 { - 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 { - let result: RunResult; - 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()]; - result = db.run(query, params); - } 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.`); - } - 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; - } - - /** - * 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.'); - } - } - } -} diff --git a/electron/database/repositories/user.repository.ts b/electron/database/repositories/user.repository.ts deleted file mode 100644 index f8e1d15..0000000 --- a/electron/database/repositories/user.repository.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm'; -import System from "../System.js"; - -export interface UserInfosQueryResponse extends Record { - first_name: string; - last_name: string; - username: string; - email: string; - plateform: string; - term_accepted: number; - account_verified: number; - author_name: string; - writing_lang: number; - writing_level: number; - rite_points: number; - user_group: number; -} - -export interface CredentialResponse { - valid: boolean; - message?: string; - user?: UserResponse; -} - -interface UserResponse { - id: string; - name: string; - last_name: string; - username: string; - email: string; - account_verified: boolean; -} - -export interface UserAccountQuery extends Record { - first_name: string; - last_name: string; - username: string; - author_name: string; - email: string; -} - -export interface GuideTourResult extends Record { - step_tour: string; -} - -export default class UserRepo { - - /** - * Inserts a new user into the database. - * @param uuId - The unique identifier for the user - * @param firstName - The user's first name - * @param lastName - The user's last name - * @param username - The user's username - * @param originUsername - The original username from the source platform - * @param email - The user's email address - * @param originEmail - The original email from the source platform - * @param lang - The language for error messages ('fr' or 'en') - * @returns The user's UUID if insertion was successful - * @throws Error if the user cannot be registered - */ - public static insertUser( - uuId: string, - firstName: string, - lastName: string, - username: string, - originUsername: string, - email: string, - originEmail: string, - lang: 'fr' | 'en' = 'fr' - ): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = ` - INSERT INTO erit_users ( - user_id, first_name, last_name, username, email, origin_email, - origin_username, plateform, term_accepted, account_verified, reg_date - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `; - const params: SQLiteValue[] = [ - uuId, - firstName, - lastName, - username, - email, - originEmail, - originUsername, - 'desktop', - 0, - 1, - Date.now() - ]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'enregistrer l'utilisateur.` : `Unable to register user.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (insertResult.changes > 0) { - return uuId; - } else { - throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'enregistrement de l'utilisateur.` : `Error registering user.`); - } - } - - /** - * Fetches user information from the database. - * @param userId - The unique identifier of the user to fetch - * @param lang - The language for error messages ('fr' or 'en') - * @returns The user information object - * @throws Error if the user is not found or cannot be retrieved - */ - public static fetchUserInfos(userId: string, lang: 'fr' | 'en' = 'fr'): UserInfosQueryResponse { - let userInfo: UserInfosQueryResponse | undefined; - try { - const db: Database = System.getDb(); - const query: string = ` - SELECT first_name, last_name, username, email, plateform, term_accepted, - account_verified, author_name, erite_points AS rite_points, user_group - FROM erit_users - WHERE user_id = ? - `; - const params: SQLiteValue[] = [userId]; - userInfo = db.get(query, params) as UserInfosQueryResponse | undefined; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les informations utilisateur.` : `Unable to retrieve user information.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!userInfo) { - throw new Error(lang === 'fr' ? `Utilisateur non trouvé.` : `User not found.`); - } - return userInfo as UserInfosQueryResponse; - } - - /** - * Updates user information in the database. - * @param userId - The unique identifier of the user to update - * @param firstName - The new first name - * @param lastName - The new last name - * @param username - The new username - * @param originUsername - The original username from the source platform - * @param email - The new email address - * @param originEmail - The original email from the source platform - * @param originalAuthorName - The original author name - * @param authorName - The new author name - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the update was successful, false otherwise - * @throws Error if the update fails - */ - public static updateUserInfos( - userId: string, - firstName: string, - lastName: string, - username: string, - originUsername: string, - email: string, - originEmail: string, - originalAuthorName: string, - authorName: string, - lang: 'fr' | 'en' = 'fr' - ): boolean { - try { - const db: Database = System.getDb(); - const query: string = ` - UPDATE erit_users - SET first_name = ?, last_name = ?, username = ?, email = ?, - origin_username = ?, origin_author_name = ?, author_name = ? - WHERE user_id = ? AND origin_email = ? - `; - const params: SQLiteValue[] = [ - firstName, - lastName, - username, - email, - originUsername, - originalAuthorName, - authorName, - userId, - originEmail - ]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour les informations utilisateur.` : `Unable to update user information.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches account information for a user. - * @param userId - The unique identifier of the user - * @param lang - The language for error messages ('fr' or 'en') - * @returns The user account information object - * @throws Error if the account is not found or cannot be retrieved - */ - public static fetchAccountInformation(userId: string, lang: 'fr' | 'en' = 'fr'): UserAccountQuery { - let accountInfo: UserAccountQuery | undefined; - try { - const db: Database = System.getDb(); - const query: string = ` - SELECT first_name, last_name, username, author_name, email - FROM erit_users - WHERE user_id = ? - `; - const params: SQLiteValue[] = [userId]; - accountInfo = db.get(query, params) as UserAccountQuery | undefined; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les informations du compte.` : `Unable to retrieve account information.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!accountInfo) { - throw new Error(lang === 'fr' ? `Compte non trouvé.` : `Account not found.`); - } - return accountInfo as UserAccountQuery; - } -} diff --git a/electron/database/repositories/world.repository.ts b/electron/database/repositories/world.repository.ts deleted file mode 100644 index 831cf41..0000000 --- a/electron/database/repositories/world.repository.ts +++ /dev/null @@ -1,583 +0,0 @@ -import { Database, QueryResult, RunResult, SQLiteValue } from "node-sqlite3-wasm"; -import System from "../System.js"; - -export interface BookWorldTable extends Record { - world_id: string; - name: string; - hashed_name: string; - author_id: string; - book_id: string; - history: string | null; - politics: string | null; - economy: string | null; - religion: string | null; - languages: string | null; - last_update: number; -} - -export interface BookWorldElementsTable extends Record { - 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 SyncedWorldResult extends Record { - world_id: string; - book_id: string; - name: string; - last_update: number; -} - -export interface SyncedWorldElementResult extends Record { - element_id: string; - world_id: string; - name: string; - last_update: number; -} - -export interface WorldQuery extends Record { - world_id: string; - world_name: string; - history: string | null; - politics: string | null; - economy: string | null; - religion: string | null; - languages: string | null; - element_id: string | null; - element_name: string | null; - element_description: string | null; - element_type: number | null; - series_world_id: string | null; -} - -export interface WorldElementValue { - id: string; - name: string; - description: string; - type: number; -} - -export default class WorldRepository { - /** - * Checks if a world with the given name exists for a specific user and book. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param worldName - The hashed name of the world to check - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the world exists, false otherwise - */ - public static checkWorldExist(userId: string, bookId: string, worldName: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT world_id FROM book_world WHERE author_id=? AND book_id=? AND hashed_name=?'; - const params: SQLiteValue[] = [userId, bookId, worldName]; - const world: QueryResult | null = db.get(query, params); - return world !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du monde.` : `Unable to verify world existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new world into the database. - * @param worldId - The unique identifier for the new world - * @param userId - The unique identifier of the author - * @param bookId - The unique identifier of the book - * @param encryptedName - The encrypted name of the world - * @param hashedName - The hashed name of the world for uniqueness checks - * @param lang - The language for error messages ('fr' or 'en') - * @returns The world ID if insertion was successful - */ - public static insertNewWorld(worldId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en', seriesWorldId: string | null = null): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = seriesWorldId - ? 'INSERT INTO book_world (world_id, author_id, book_id, name, hashed_name, series_world_id, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)' - : 'INSERT INTO book_world (world_id, author_id, book_id, name, hashed_name, last_update) VALUES (?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = seriesWorldId - ? [worldId, userId, bookId, encryptedName, hashedName, seriesWorldId, System.timeStampInSeconds()] - : [worldId, userId, bookId, encryptedName, hashedName, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter le monde.` : `Unable to add world.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du monde.` : `Error adding world.`); - } - return worldId; - } - - /** - * Fetches all worlds and their elements for a specific user and 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 world query results with joined element data - */ - public static fetchWorlds(userId: string, bookId: string, lang: 'fr' | 'en'): WorldQuery[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT world.world_id AS world_id, world.name AS world_name, world.history, world.politics, world.economy, world.religion, world.languages, element.element_id AS element_id, element.name AS element_name, element.description AS element_description, element.element_type, world.series_world_id FROM book_world AS world LEFT JOIN book_world_elements AS element ON world.world_id=element.world_id WHERE world.author_id=? AND world.book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const worlds: WorldQuery[] = db.all(query, params) as WorldQuery[]; - return worlds; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les mondes.` : `Unable to retrieve worlds.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a world's data in the database. - * @param userId - The unique identifier of the author - * @param worldId - The unique identifier of the world to update - * @param encryptName - The new encrypted name - * @param hashedName - The new hashed name - * @param encryptHistory - The new encrypted history - * @param encryptPolitics - The new encrypted politics - * @param encryptEconomy - The new encrypted economy - * @param encryptReligion - The new encrypted religion - * @param encryptLanguages - The new encrypted languages - * @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, false otherwise - */ - public static updateWorld(userId: string, worldId: string, encryptName: string, hashedName: string, encryptHistory: string, encryptPolitics: string, encryptEconomy: string, encryptReligion: string, encryptLanguages: string, lastUpdate: number, lang: 'fr' | 'en', seriesWorldId: string | null = null): boolean { - try { - const db: Database = System.getDb(); - const query: string = seriesWorldId !== null - ? 'UPDATE book_world SET name=?, hashed_name=?, history=?, politics=?, economy=?, religion=?, languages=?, last_update=?, series_world_id=? WHERE author_id=? AND world_id=?' - : 'UPDATE book_world SET name=?, hashed_name=?, history=?, politics=?, economy=?, religion=?, languages=?, last_update=? WHERE author_id=? AND world_id=?'; - const params: SQLiteValue[] = seriesWorldId !== null - ? [encryptName, hashedName, encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, lastUpdate, seriesWorldId, userId, worldId] - : [encryptName, hashedName, encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, lastUpdate, userId, worldId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour le monde.` : `Unable to update world.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates multiple world elements in the database. - * @param userId - The unique identifier of the user - * @param elements - An array of world element values to update - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if all updates were successful, false otherwise - */ - public static updateWorldElements(userId: string, elements: WorldElementValue[], lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'UPDATE book_world_elements SET name=?, description=?, element_type=?, last_update=? WHERE user_id=? AND element_id=?'; - for (const element of elements) { - const params: SQLiteValue[] = [element.name, element.description, element.type, System.timeStampInSeconds(), userId, element.id]; - const updateResult: RunResult = db.run(query, params); - if (updateResult.changes <= 0) { - return false; - } - } - return true; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour les éléments du monde.` : `Unable to update world elements.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a world element with the given hashed name exists. - * @param worldNumId - The unique identifier of the world - * @param hashedName - The hashed name of the element to check - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the element exists, false otherwise - */ - public static checkElementExist(worldNumId: string, hashedName: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT element_id FROM book_world_elements WHERE world_id=? AND original_name=?'; - const params: SQLiteValue[] = [worldNumId, hashedName]; - const element: QueryResult | null = db.get(query, params); - return element !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'élément.` : `Unable to verify element existence.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a new world element into the database. - * @param userId - The unique identifier of the user - * @param elementId - The unique identifier for the new element - * @param elementType - The type of the element - * @param worldId - The unique identifier of the parent world - * @param encryptedName - The encrypted name of the element - * @param hashedName - The hashed name of the element for uniqueness checks - * @param lang - The language for error messages ('fr' or 'en') - * @returns The element ID if insertion was successful - */ - public static insertNewElement(userId: string, elementId: string, elementType: number, worldId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en'): string { - let insertResult: RunResult; - try { - const db: Database = System.getDb(); - const query: string = 'INSERT INTO book_world_elements (element_id, world_id, user_id, name, original_name, element_type, last_update) VALUES (?, ?, ?, ?, ?, ?, ?)'; - const params: SQLiteValue[] = [elementId, worldId, userId, encryptedName, hashedName, elementType, System.timeStampInSeconds()]; - insertResult = db.run(query, params); - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'ajouter l'élément.` : `Unable to add element.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - if (!insertResult || insertResult.changes === 0) { - throw new Error(lang === 'fr' ? `Erreur lors de l'ajout de l'élément.` : `Error adding element.`); - } - return elementId; - } - - /** - * Deletes a world element from the database. - * @param userId - The unique identifier of the user - * @param elementId - The unique identifier of the element to delete - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the deletion was successful, false otherwise - */ - public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'DELETE FROM book_world_elements WHERE user_id=? AND element_id=?'; - const params: SQLiteValue[] = [userId, elementId]; - const deleteResult: RunResult = db.run(query, params); - return deleteResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de supprimer l'élément.` : `Unable to delete element.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all worlds for a specific user and 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 A promise resolving to an array of book world table records - */ - static async fetchBookWorlds(userId: string, bookId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT world_id, name, hashed_name, author_id, book_id, history, politics, economy, religion, languages, last_update FROM book_world WHERE author_id=? AND book_id=?'; - const params: SQLiteValue[] = [userId, bookId]; - const worlds: BookWorldTable[] = db.all(query, params) as BookWorldTable[]; - return worlds; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les mondes.` : `Unable to retrieve worlds.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all elements for a specific world. - * @param userId - The unique identifier of the user - * @param worldId - The unique identifier of the world - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of book world elements table records - */ - static async fetchBookWorldElements(userId: string, worldId: string, lang: 'fr' | 'en'): Promise { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM book_world_elements WHERE user_id=? AND world_id=?'; - const params: SQLiteValue[] = [userId, worldId]; - const elements: BookWorldElementsTable[] = db.all(query, params) as BookWorldElementsTable[]; - return elements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments du monde.` : `Unable to retrieve world elements.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced worlds for a specific user. - * @param userId - The unique identifier of the user - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of synced world results - */ - static fetchSyncedWorlds(userId: string, lang: 'fr' | 'en'): SyncedWorldResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT world_id, book_id, name, last_update FROM book_world WHERE author_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedWorlds: SyncedWorldResult[] = db.all(query, params) as SyncedWorldResult[]; - return syncedWorlds; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les mondes synchronisés.` : `Unable to retrieve synced worlds.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches all synced world elements for a specific user. - * @param userId - The unique identifier of the user - * @param lang - The language for error messages ('fr' or 'en') - * @returns An array of synced world element results - */ - static fetchSyncedWorldElements(userId: string, lang: 'fr' | 'en'): SyncedWorldElementResult[] { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT element_id, world_id, name, last_update FROM book_world_elements WHERE user_id = ?'; - const params: SQLiteValue[] = [userId]; - const syncedElements: SyncedWorldElementResult[] = db.all(query, params) as SyncedWorldElementResult[]; - return syncedElements; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de récupérer les éléments de monde synchronisés.` : `Unable to retrieve synced world elements.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a synced world into the database. - * @param worldId - The unique identifier for the world - * @param name - The encrypted name of the world - * @param hashedName - The hashed name of the world - * @param authorId - The unique identifier of the author - * @param bookId - The unique identifier of the book - * @param history - The encrypted history (optional) - * @param politics - The encrypted politics (optional) - * @param economy - The encrypted economy (optional) - * @param religion - The encrypted religion (optional) - * @param languages - The encrypted languages (optional) - * @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, false otherwise - */ - static insertSyncWorld(worldId: string, name: string, hashedName: string, authorId: string, bookId: string, history: string | null, politics: string | null, economy: string | null, religion: string | null, languages: string | null, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = `INSERT INTO book_world (world_id, name, hashed_name, author_id, book_id, history, politics, economy, religion, languages, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; - const params: SQLiteValue[] = [worldId, name, hashedName, authorId, bookId, history, politics, economy, religion, languages, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer le monde.` : `Unable to insert world.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Inserts a synced world element into the database. - * @param elementId - The unique identifier for the element - * @param worldId - The unique identifier of the parent world - * @param userId - The unique identifier of the user - * @param elementType - The type of the element - * @param name - The encrypted name of the element - * @param originalName - The original hashed name - * @param description - The encrypted description (optional) - * @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, false otherwise - */ - static insertSyncWorldElement(elementId: string, worldId: string, userId: string, elementType: number, name: string, originalName: string, description: string | null, lastUpdate: number, lang: 'fr' | 'en'): boolean { - try { - const db: Database = System.getDb(); - const query: string = `INSERT INTO book_world_elements (element_id, world_id, user_id, element_type, name, original_name, description, last_update) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`; - const params: SQLiteValue[] = [elementId, worldId, userId, elementType, name, originalName, description, lastUpdate]; - const insertResult: RunResult = db.run(query, params); - return insertResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible d'insérer l'élément du monde.` : `Unable to insert world element.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete world by its ID. - * @param id - The unique identifier of the world - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of book world table records - */ - static async fetchCompleteWorldById(id: string, lang: "fr" | "en"): Promise { - try { - const db: Database = System.getDb(); - const query: string = `SELECT world_id, name, hashed_name, author_id, book_id, history, politics, economy, religion, languages, last_update FROM book_world WHERE world_id = ?`; - const params: SQLiteValue[] = [id]; - const worlds: BookWorldTable[] = db.all(query, params) as BookWorldTable[]; - return worlds; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? `Impossible de récupérer le monde complet.` : `Unable to retrieve complete world.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Fetches a complete world element by its ID. - * @param id - The unique identifier of the element - * @param lang - The language for error messages ('fr' or 'en') - * @returns A promise resolving to an array of book world elements table records - */ - static async fetchCompleteWorldElementById(id: string, lang: "fr" | "en"): Promise { - try { - const db: Database = System.getDb(); - const query: string = `SELECT element_id, world_id, user_id, element_type, name, original_name, description, last_update FROM book_world_elements WHERE element_id = ?`; - const params: SQLiteValue[] = [id]; - const elements: BookWorldElementsTable[] = db.all(query, params) as BookWorldElementsTable[]; - return elements; - } catch (error: unknown) { - if (error instanceof Error) { - throw new Error(lang === 'fr' ? `Impossible de récupérer l'élément de monde complet.` : `Unable to retrieve complete world element.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Updates a single world element's name and description. - * @param userId - The unique identifier of the user - * @param elementId - The unique identifier of the element to update - * @param name - The new encrypted name - * @param description - The new encrypted description - * @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, false otherwise - */ - static updateWorldElement(userId: string, elementId: string, name: string, description: string, lastUpdate: number, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = `UPDATE book_world_elements SET name = ?, description = ?, last_update = ? WHERE element_id = ? AND user_id = ?`; - const params: SQLiteValue[] = [name, description, lastUpdate, elementId, userId]; - const updateResult: RunResult = db.run(query, params); - return updateResult.changes > 0; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de mettre à jour l'élément du monde.` : `Unable to update world element.`); - } else { - console.error("An unknown error occurred."); - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a world exists for a specific user and book. - * @param userId - The unique identifier of the user - * @param bookId - The unique identifier of the book - * @param worldId - The unique identifier of the world - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the world exists, false otherwise - */ - static worldExist(userId: string, bookId: string, worldId: string, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM book_world WHERE world_id=? AND author_id=? AND book_id=?'; - const params: SQLiteValue[] = [worldId, userId, bookId]; - const world: QueryResult | null = db.get(query, params) || null; - return world !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du monde.` : `Unable to check world existence.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } - - /** - * Checks if a world element exists for a specific user and world. - * @param userId - The unique identifier of the user - * @param worldId - The unique identifier of the world - * @param elementId - The unique identifier of the element - * @param lang - The language for error messages ('fr' or 'en') - * @returns True if the element exists, false otherwise - */ - static worldElementExist(userId: string, worldId: string, elementId: string, lang: "fr" | "en"): boolean { - try { - const db: Database = System.getDb(); - const query: string = 'SELECT 1 FROM book_world_elements WHERE element_id=? AND world_id=? AND user_id=?'; - const params: SQLiteValue[] = [elementId, worldId, userId]; - const element: QueryResult | null = db.get(query, params) || null; - return element !== null; - } catch (error: unknown) { - if (error instanceof Error) { - console.error(`DB Error: ${error.message}`); - throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'élément du monde.` : `Unable to check world element existence.`); - } else { - throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); - } - } - } -} diff --git a/electron/database/schema.ts b/electron/database/schema.ts deleted file mode 100644 index d2e5dc4..0000000 --- a/electron/database/schema.ts +++ /dev/null @@ -1,1320 +0,0 @@ -import sqlite3 from 'node-sqlite3-wasm'; -import { app } from 'electron'; - -type Database = sqlite3.Database; - -/** - * SQLite schema based on the MySQL erit_main_db schema - * All tables use snake_case naming to match the server database - * Data is encrypted before storage and decrypted on retrieval - */ - -// ============================================================================= -// MIGRATIONS -// ============================================================================= - -const schemaVersion = 3; - -/** - * DEV ONLY - S'exécute à chaque refresh, pas besoin de version - * Mets ta query, test, efface après - */ -const devQueries: string[] = [ - // V3 Migration: Series tables and series_*_id columns - - // Book Series - `CREATE TABLE IF NOT EXISTS book_series ( - series_id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - hashed_name TEXT NOT NULL, - description TEXT, - cover_image TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_book_series_user ON book_series(user_id)`, - - // Series Books - `CREATE TABLE IF NOT EXISTS series_books ( - series_id TEXT NOT NULL, - book_id TEXT NOT NULL, - book_order INTEGER NOT NULL DEFAULT 1, - last_update INTEGER DEFAULT 0, - PRIMARY KEY (series_id, book_id), - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_series_books_book ON series_books(book_id)`, - - // Series Characters - `CREATE TABLE IF NOT EXISTS series_characters ( - character_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - first_name TEXT NOT NULL, - last_name TEXT, - nickname TEXT, - age TEXT, - gender TEXT, - species TEXT, - nationality TEXT, - status TEXT, - category TEXT NOT NULL, - title TEXT, - image TEXT, - role TEXT, - biography TEXT, - history TEXT, - speech_pattern TEXT, - catchphrase TEXT, - residence TEXT, - notes TEXT, - color TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_series_characters_series ON series_characters(series_id)`, - `CREATE INDEX IF NOT EXISTS idx_series_characters_user ON series_characters(user_id)`, - - // Series Characters Attributes - `CREATE TABLE IF NOT EXISTS series_characters_attributes ( - attr_id TEXT PRIMARY KEY, - character_id TEXT NOT NULL, - user_id TEXT NOT NULL, - attribute_name TEXT NOT NULL, - attribute_value TEXT NOT NULL, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (character_id) REFERENCES series_characters(character_id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_series_char_attrs_character ON series_characters_attributes(character_id)`, - `CREATE INDEX IF NOT EXISTS idx_series_char_attrs_user ON series_characters_attributes(user_id)`, - - // Series Worlds - `CREATE TABLE IF NOT EXISTS series_worlds ( - world_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - hashed_name TEXT NOT NULL, - history TEXT, - politics TEXT, - economy TEXT, - religion TEXT, - languages TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_series_worlds_series ON series_worlds(series_id)`, - `CREATE INDEX IF NOT EXISTS idx_series_worlds_user ON series_worlds(user_id)`, - - // Series World Elements - `CREATE TABLE IF NOT EXISTS series_world_elements ( - element_id TEXT PRIMARY KEY, - world_id TEXT NOT NULL, - user_id TEXT NOT NULL, - element_type INTEGER NOT NULL, - name TEXT NOT NULL, - original_name TEXT NOT NULL, - description TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (world_id) REFERENCES series_worlds(world_id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_series_world_elements_world ON series_world_elements(world_id)`, - `CREATE INDEX IF NOT EXISTS idx_series_world_elements_user ON series_world_elements(user_id)`, - - // Series Locations - `CREATE TABLE IF NOT EXISTS series_locations ( - loc_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - loc_name TEXT NOT NULL, - loc_original_name TEXT NOT NULL, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_series_locations_series ON series_locations(series_id)`, - `CREATE INDEX IF NOT EXISTS idx_series_locations_user ON series_locations(user_id)`, - - // Series Location Elements - `CREATE TABLE IF NOT EXISTS series_location_elements ( - element_id TEXT PRIMARY KEY, - location_id TEXT NOT NULL, - user_id TEXT NOT NULL, - element_name TEXT NOT NULL, - original_name TEXT NOT NULL, - element_description TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (location_id) REFERENCES series_locations(loc_id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_series_loc_elements_location ON series_location_elements(location_id)`, - `CREATE INDEX IF NOT EXISTS idx_series_loc_elements_user ON series_location_elements(user_id)`, - - // Series Location Sub Elements - `CREATE TABLE IF NOT EXISTS series_location_sub_elements ( - sub_element_id TEXT PRIMARY KEY, - element_id TEXT NOT NULL, - user_id TEXT NOT NULL, - sub_elem_name TEXT NOT NULL, - original_name TEXT NOT NULL, - sub_elem_description TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (element_id) REFERENCES series_location_elements(element_id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)`, - `CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`, - - // Series Spells - `CREATE TABLE IF NOT EXISTS series_spells ( - spell_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - name_hash TEXT NOT NULL, - description TEXT, - appearance TEXT, - tags TEXT, - power_level TEXT, - components TEXT, - limitations TEXT, - notes TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`, - `CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`, - - // Series Spell Tags - `CREATE TABLE IF NOT EXISTS series_spell_tags ( - tag_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - hashed_name TEXT NOT NULL, - color TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - )`, - `CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)`, - `CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)`, - - // Add series_*_id columns to existing book tables (will fail silently if already exists) - `ALTER TABLE book_characters ADD COLUMN series_character_id TEXT DEFAULT NULL`, - `ALTER TABLE book_world ADD COLUMN series_world_id TEXT DEFAULT NULL`, - `ALTER TABLE book_location ADD COLUMN series_location_id TEXT DEFAULT NULL`, - `ALTER TABLE book_spells ADD COLUMN series_spell_id TEXT DEFAULT NULL`, - - // Removed Items (sync deletion tracking) - `CREATE TABLE IF NOT EXISTS removed_items ( - removal_id TEXT PRIMARY KEY, - table_name TEXT NOT NULL, - entity_id TEXT NOT NULL, - book_id TEXT, - user_id TEXT NOT NULL, - deleted_at INTEGER NOT NULL - )`, - `CREATE INDEX IF NOT EXISTS idx_removed_items_user ON removed_items(user_id)`, - `CREATE INDEX IF NOT EXISTS idx_removed_items_book ON removed_items(book_id)`, - `CREATE INDEX IF NOT EXISTS idx_removed_items_deleted_at ON removed_items(deleted_at)`, - `CREATE UNIQUE INDEX IF NOT EXISTS idx_removed_items_entity ON removed_items(table_name, entity_id)`, -]; - -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 }[]; - return result?.some((row) => row.name === column) ?? false; -} - -function addColumn(db: Database, table: string, column: string, type: string): void { - if (!columnExists(db, table, column)) { - db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`); - } -} - -function getDbVersion(db: Database): number { - const result = db.get('PRAGMA user_version') as { user_version: number } | undefined; - return result?.user_version ?? 0; -} - -function setDbVersion(db: Database, version: number): void { - db.exec(`PRAGMA user_version = ${version}`); -} - -/** - * Check if old _schema_version table exists - */ -function hasOldSchemaTable(db: Database): boolean { - const result = db.get(` - SELECT name FROM sqlite_master - WHERE type='table' AND name='_schema_version' - `) as { name: string } | undefined; - return !!result; -} - -/** - * Get version from old _schema_version table - */ -function getOldSchemaVersion(db: Database): number { - try { - const result = db.get('SELECT version FROM _schema_version LIMIT 1') as { version: number } | undefined; - return result?.version ?? 0; - } catch { - return 0; - } -} - -/** - * Migrate from old _schema_version table to PRAGMA user_version - * Old system: v3 = all migrations done (book_tools created, NOT NULL fixes applied) - * New system: v1 = equivalent starting point - */ -function migrateFromOldSystem(db: Database): void { - const oldVersion = getOldSchemaVersion(db); - - // Old v3 means all previous migrations were done - // Map to new system: old v3 = new v1 - if (oldVersion >= 3) { - setDbVersion(db, schemaVersion); - } - - // Add last_update column to book_tools if missing (was added after v3) - addColumn(db, 'book_tools', 'last_update', 'INTEGER DEFAULT 0'); - - // Add spells_enabled column to book_tools if missing - addColumn(db, 'book_tools', 'spells_enabled', 'INTEGER NOT NULL DEFAULT 0'); - - // Add new character fields if missing - addColumn(db, 'book_characters', 'nickname', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'age', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'gender', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'species', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'nationality', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'status', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'speech_pattern', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'catchphrase', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'residence', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'notes', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'color', 'TEXT DEFAULT NULL'); - - // Create book_spell_tags table if missing - db.exec(` - CREATE TABLE IF NOT EXISTS book_spell_tags ( - tag_id TEXT PRIMARY KEY, - book_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - name_hash TEXT NOT NULL, - color TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_book ON book_spell_tags(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_user ON book_spell_tags(user_id)`); - - // Create book_spells table if missing - db.exec(` - CREATE TABLE IF NOT EXISTS book_spells ( - spell_id TEXT PRIMARY KEY, - book_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - name_hash TEXT NOT NULL, - description TEXT, - appearance TEXT, - tags TEXT, - power_level TEXT, - components TEXT, - limitations TEXT, - notes TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_book ON book_spells(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_user ON book_spells(user_id)`); - - // Create series tables (v3) - db.exec(`CREATE TABLE IF NOT EXISTS book_series (series_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, description TEXT, cover_image TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_book_series_user ON book_series(user_id)`); - - db.exec(`CREATE TABLE IF NOT EXISTS series_books (series_id TEXT NOT NULL, book_id TEXT NOT NULL, book_order INTEGER NOT NULL DEFAULT 1, last_update INTEGER DEFAULT 0, PRIMARY KEY (series_id, book_id), FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_books_book ON series_books(book_id)`); - - db.exec(`CREATE TABLE IF NOT EXISTS series_characters (character_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, first_name TEXT NOT NULL, last_name TEXT, nickname TEXT, age TEXT, gender TEXT, species TEXT, nationality TEXT, status TEXT, category TEXT NOT NULL, title TEXT, image TEXT, role TEXT, biography TEXT, history TEXT, speech_pattern TEXT, catchphrase TEXT, residence TEXT, notes TEXT, color TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_series ON series_characters(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_user ON series_characters(user_id)`); - - db.exec(`CREATE TABLE IF NOT EXISTS series_characters_attributes (attr_id TEXT PRIMARY KEY, character_id TEXT NOT NULL, user_id TEXT NOT NULL, attribute_name TEXT NOT NULL, attribute_value TEXT NOT NULL, last_update INTEGER DEFAULT 0, FOREIGN KEY (character_id) REFERENCES series_characters(character_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_character ON series_characters_attributes(character_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_user ON series_characters_attributes(user_id)`); - - db.exec(`CREATE TABLE IF NOT EXISTS series_worlds (world_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, history TEXT, politics TEXT, economy TEXT, religion TEXT, languages TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_series ON series_worlds(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_user ON series_worlds(user_id)`); - - db.exec(`CREATE TABLE IF NOT EXISTS series_world_elements (element_id TEXT PRIMARY KEY, world_id TEXT NOT NULL, user_id TEXT NOT NULL, element_type INTEGER NOT NULL, name TEXT NOT NULL, original_name TEXT NOT NULL, description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (world_id) REFERENCES series_worlds(world_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_world ON series_world_elements(world_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_user ON series_world_elements(user_id)`); - - db.exec(`CREATE TABLE IF NOT EXISTS series_locations (loc_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, loc_name TEXT NOT NULL, loc_original_name TEXT NOT NULL, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_series ON series_locations(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_user ON series_locations(user_id)`); - - db.exec(`CREATE TABLE IF NOT EXISTS series_location_elements (element_id TEXT PRIMARY KEY, location_id TEXT NOT NULL, user_id TEXT NOT NULL, element_name TEXT NOT NULL, original_name TEXT NOT NULL, element_description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (location_id) REFERENCES series_locations(loc_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_location ON series_location_elements(location_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_user ON series_location_elements(user_id)`); - - db.exec(`CREATE TABLE IF NOT EXISTS series_location_sub_elements (sub_element_id TEXT PRIMARY KEY, element_id TEXT NOT NULL, user_id TEXT NOT NULL, sub_elem_name TEXT NOT NULL, original_name TEXT NOT NULL, sub_elem_description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (element_id) REFERENCES series_location_elements(element_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`); - - db.exec(`CREATE TABLE IF NOT EXISTS series_spells (spell_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, name_hash TEXT NOT NULL, description TEXT, appearance TEXT, tags TEXT, power_level TEXT, components TEXT, limitations TEXT, notes TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`); - - db.exec(`CREATE TABLE IF NOT EXISTS series_spell_tags (tag_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, color TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)`); - - // Add series_*_id columns to existing book tables (v3) - addColumn(db, 'book_characters', 'series_character_id', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_world', 'series_world_id', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_location', 'series_location_id', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_spells', 'series_spell_id', 'TEXT DEFAULT NULL'); - - // Removed Items (sync deletion tracking) - db.exec(` - CREATE TABLE IF NOT EXISTS removed_items ( - removal_id TEXT PRIMARY KEY, - table_name TEXT NOT NULL, - entity_id TEXT NOT NULL, - book_id TEXT, - user_id TEXT NOT NULL, - deleted_at INTEGER NOT NULL, - removed_time INTEGER NOT NULL DEFAULT 0 - ) - `); - db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_user ON removed_items(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_book ON removed_items(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_deleted_at ON removed_items(deleted_at)`); - db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_removed_items_entity ON removed_items(table_name, entity_id)`); - - // Drop old schema version table - db.exec('DROP TABLE IF EXISTS _schema_version'); -} - -export function runMigrations(db: Database): void { - // DEV: run test queries (skip errors silently) - if (isDev && devQueries.length > 0) { - for (const query of devQueries) { - try { - db.exec(query); - } catch (_) { /* ignore */ } - } - } - - // PROD only: versioned migrations - if (isDev) return; - - // Migrate from old _schema_version system to PRAGMA user_version - if (hasOldSchemaTable(db)) { - migrateFromOldSystem(db); - return; // Migration done, no need to continue - } - - const currentVersion = getDbVersion(db); - if (currentVersion >= schemaVersion) return; - - // v1 - book_tools table + spell book tables (for fresh DBs or DBs without old system) - if (currentVersion < 1) { - // Book Tools - db.exec(` - CREATE TABLE IF NOT EXISTS book_tools ( - book_id TEXT NOT NULL, - user_id TEXT NOT NULL, - characters_enabled INTEGER NOT NULL DEFAULT 0, - worlds_enabled INTEGER NOT NULL DEFAULT 0, - locations_enabled INTEGER NOT NULL DEFAULT 0, - spells_enabled INTEGER NOT NULL DEFAULT 0, - last_update INTEGER DEFAULT 0, - UNIQUE (book_id, user_id), - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_book ON book_tools(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_user ON book_tools(user_id)`); - - // Book Spell Tags - db.exec(` - CREATE TABLE IF NOT EXISTS book_spell_tags ( - tag_id TEXT PRIMARY KEY, - book_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - name_hash TEXT NOT NULL, - color TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_book ON book_spell_tags(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_user ON book_spell_tags(user_id)`); - - // Book Spells - db.exec(` - CREATE TABLE IF NOT EXISTS book_spells ( - spell_id TEXT PRIMARY KEY, - book_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - name_hash TEXT NOT NULL, - description TEXT, - appearance TEXT, - tags TEXT, - power_level TEXT, - components TEXT, - limitations TEXT, - notes TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_book ON book_spells(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_user ON book_spells(user_id)`); - } - - // v2 - Add new character fields (nickname, age, gender, species, nationality, status, etc.) - if (currentVersion < 2) { - addColumn(db, 'book_characters', 'nickname', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'age', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'gender', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'species', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'nationality', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'status', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'speech_pattern', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'catchphrase', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'residence', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'notes', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_characters', 'color', 'TEXT DEFAULT NULL'); - } - - // v3 - Add series tables and series_*_id columns to existing book tables - if (currentVersion < 3) { - // Create series tables first (order matters for foreign keys) - - // Book Series (main series table) - db.exec(`CREATE TABLE IF NOT EXISTS book_series (series_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, description TEXT, cover_image TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_book_series_user ON book_series(user_id)`); - - // Series Books (link series to books with order) - db.exec(`CREATE TABLE IF NOT EXISTS series_books (series_id TEXT NOT NULL, book_id TEXT NOT NULL, book_order INTEGER NOT NULL DEFAULT 1, last_update INTEGER DEFAULT 0, PRIMARY KEY (series_id, book_id), FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_books_book ON series_books(book_id)`); - - // Series Characters - db.exec(`CREATE TABLE IF NOT EXISTS series_characters (character_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, first_name TEXT NOT NULL, last_name TEXT, nickname TEXT, age TEXT, gender TEXT, species TEXT, nationality TEXT, status TEXT, category TEXT NOT NULL, title TEXT, image TEXT, role TEXT, biography TEXT, history TEXT, speech_pattern TEXT, catchphrase TEXT, residence TEXT, notes TEXT, color TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_series ON series_characters(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_user ON series_characters(user_id)`); - - // Series Characters Attributes - db.exec(`CREATE TABLE IF NOT EXISTS series_characters_attributes (attr_id TEXT PRIMARY KEY, character_id TEXT NOT NULL, user_id TEXT NOT NULL, attribute_name TEXT NOT NULL, attribute_value TEXT NOT NULL, last_update INTEGER DEFAULT 0, FOREIGN KEY (character_id) REFERENCES series_characters(character_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_character ON series_characters_attributes(character_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_user ON series_characters_attributes(user_id)`); - - // Series Worlds - db.exec(`CREATE TABLE IF NOT EXISTS series_worlds (world_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, history TEXT, politics TEXT, economy TEXT, religion TEXT, languages TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_series ON series_worlds(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_user ON series_worlds(user_id)`); - - // Series World Elements - db.exec(`CREATE TABLE IF NOT EXISTS series_world_elements (element_id TEXT PRIMARY KEY, world_id TEXT NOT NULL, user_id TEXT NOT NULL, element_type INTEGER NOT NULL, name TEXT NOT NULL, original_name TEXT NOT NULL, description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (world_id) REFERENCES series_worlds(world_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_world ON series_world_elements(world_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_user ON series_world_elements(user_id)`); - - // Series Locations - db.exec(`CREATE TABLE IF NOT EXISTS series_locations (loc_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, loc_name TEXT NOT NULL, loc_original_name TEXT NOT NULL, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_series ON series_locations(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_user ON series_locations(user_id)`); - - // Series Location Elements - db.exec(`CREATE TABLE IF NOT EXISTS series_location_elements (element_id TEXT PRIMARY KEY, location_id TEXT NOT NULL, user_id TEXT NOT NULL, element_name TEXT NOT NULL, original_name TEXT NOT NULL, element_description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (location_id) REFERENCES series_locations(loc_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_location ON series_location_elements(location_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_user ON series_location_elements(user_id)`); - - // Series Location Sub Elements - db.exec(`CREATE TABLE IF NOT EXISTS series_location_sub_elements (sub_element_id TEXT PRIMARY KEY, element_id TEXT NOT NULL, user_id TEXT NOT NULL, sub_elem_name TEXT NOT NULL, original_name TEXT NOT NULL, sub_elem_description TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (element_id) REFERENCES series_location_elements(element_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`); - - // Series Spells - db.exec(`CREATE TABLE IF NOT EXISTS series_spells (spell_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, name_hash TEXT NOT NULL, description TEXT, appearance TEXT, tags TEXT, power_level TEXT, components TEXT, limitations TEXT, notes TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`); - - // Series Spell Tags - db.exec(`CREATE TABLE IF NOT EXISTS series_spell_tags (tag_id TEXT PRIMARY KEY, series_id TEXT NOT NULL, user_id TEXT NOT NULL, name TEXT NOT NULL, hashed_name TEXT NOT NULL, color TEXT, last_update INTEGER DEFAULT 0, FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE);`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)`); - - // Add series_*_id columns to existing book tables - addColumn(db, 'book_characters', 'series_character_id', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_world', 'series_world_id', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_location', 'series_location_id', 'TEXT DEFAULT NULL'); - addColumn(db, 'book_spells', 'series_spell_id', 'TEXT DEFAULT NULL'); - - // Removed Items (sync deletion tracking) - db.exec(` - CREATE TABLE IF NOT EXISTS removed_items ( - removal_id TEXT PRIMARY KEY, - table_name TEXT NOT NULL, - entity_id TEXT NOT NULL, - book_id TEXT, - user_id TEXT NOT NULL, - deleted_at INTEGER NOT NULL, - removed_time INTEGER NOT NULL DEFAULT 0 - ) - `); - db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_user ON removed_items(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_book ON removed_items(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_deleted_at ON removed_items(deleted_at)`); - db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_removed_items_entity ON removed_items(table_name, entity_id)`); - } - - setDbVersion(db, schemaVersion); -} - -/** - * Initialize the local SQLite database with all required tables - * @param db - SQLite database instance - */ -export function initializeSchema(db: Database): void { - // Enable foreign keys - db.exec('PRAGMA foreign_keys = ON'); - - // AI Conversations - db.exec(` - CREATE TABLE IF NOT EXISTS ai_conversations ( - conversation_id TEXT PRIMARY KEY, - book_id TEXT NOT NULL, - mode TEXT NOT NULL, - title TEXT NOT NULL, - start_date INTEGER NOT NULL, - status INTEGER NOT NULL, - user_id TEXT NOT NULL, - summary TEXT, - convo_meta TEXT NOT NULL, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - - // AI Messages History - db.exec(` - CREATE TABLE IF NOT EXISTS ai_messages_history ( - message_id TEXT PRIMARY KEY, - conversation_id TEXT NOT NULL, - role TEXT NOT NULL, - message TEXT NOT NULL, - message_date INTEGER NOT NULL, - FOREIGN KEY (conversation_id) REFERENCES ai_conversations(conversation_id) ON DELETE CASCADE - ); - `); - - // Book Acts - db.exec(` - CREATE TABLE IF NOT EXISTS book_acts ( - act_id INTEGER PRIMARY KEY, - title TEXT NOT NULL, - last_update INTEGER DEFAULT 0 - ); - `); - - // Book Act Summaries - db.exec(` - CREATE TABLE IF NOT EXISTS book_act_summaries ( - act_sum_id TEXT PRIMARY KEY, - book_id TEXT NOT NULL, - user_id TEXT NOT NULL, - act_index INTEGER NOT NULL, - summary TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - - // Book AI Guide Line - db.exec(` - CREATE TABLE IF NOT EXISTS book_ai_guide_line ( - user_id TEXT NOT NULL, - book_id TEXT NOT NULL, - global_resume TEXT, - themes TEXT, - verbe_tense INTEGER, - narrative_type INTEGER, - langue INTEGER, - dialogue_type INTEGER, - tone TEXT, - atmosphere TEXT, - current_resume TEXT, - last_update INTEGER DEFAULT 0, - PRIMARY KEY (user_id, book_id), - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - - // Book Chapters - db.exec(` - CREATE TABLE IF NOT EXISTS book_chapters ( - chapter_id TEXT PRIMARY KEY, - book_id TEXT NOT NULL, - author_id TEXT NOT NULL, - title TEXT NOT NULL, - hashed_title TEXT, - words_count INTEGER, - chapter_order INTEGER, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - - // Book Chapter Content - db.exec(` - CREATE TABLE IF NOT EXISTS book_chapter_content ( - content_id TEXT PRIMARY KEY, - chapter_id TEXT NOT NULL, - author_id TEXT NOT NULL, - version INTEGER NOT NULL DEFAULT 2, - content TEXT, - words_count INTEGER NOT NULL, - time_on_it INTEGER NOT NULL DEFAULT 0, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE - ); - `); - - // Book Chapter Infos - db.exec(` - CREATE TABLE IF NOT EXISTS book_chapter_infos ( - chapter_info_id TEXT PRIMARY KEY, - chapter_id TEXT, - act_id INTEGER, - incident_id TEXT, - plot_point_id TEXT, - book_id TEXT, - author_id TEXT, - summary TEXT, - goal TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE, - FOREIGN KEY (incident_id) REFERENCES book_incidents(incident_id) ON DELETE CASCADE, - FOREIGN KEY (plot_point_id) REFERENCES book_plot_points(plot_point_id) ON DELETE CASCADE - ); - `); - - // Book Characters - db.exec(` - CREATE TABLE IF NOT EXISTS book_characters ( - character_id TEXT PRIMARY KEY, - book_id TEXT NOT NULL, - user_id TEXT NOT NULL, - first_name TEXT NOT NULL, - last_name TEXT, - nickname TEXT, - age TEXT, - gender TEXT, - species TEXT, - nationality TEXT, - status TEXT, - category TEXT NOT NULL, - title TEXT, - image TEXT, - role TEXT, - biography TEXT, - history TEXT, - speech_pattern TEXT, - catchphrase TEXT, - residence TEXT, - notes TEXT, - color TEXT, - series_character_id TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE, - FOREIGN KEY (series_character_id) REFERENCES series_characters(character_id) ON DELETE SET NULL - ); - `); - - // Book Character Attributes - db.exec(` - CREATE TABLE IF NOT EXISTS book_characters_attributes ( - attr_id TEXT PRIMARY KEY, - character_id TEXT NOT NULL, - user_id TEXT NOT NULL, - attribute_name TEXT NOT NULL, - attribute_value TEXT NOT NULL, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (character_id) REFERENCES book_characters(character_id) ON DELETE CASCADE - ); - `); - - // Book Character Relations - db.exec(` - CREATE TABLE IF NOT EXISTS book_characters_relations ( - rel_id INTEGER PRIMARY KEY, - character_id INTEGER NOT NULL, - char_name TEXT NOT NULL, - type TEXT NOT NULL, - description TEXT NOT NULL, - history TEXT NOT NULL, - last_update INTEGER DEFAULT 0 - ); - `); - - // Book Guide Line - db.exec(` - CREATE TABLE IF NOT EXISTS book_guide_line ( - user_id TEXT NOT NULL, - book_id TEXT NOT NULL, - tone TEXT, - atmosphere TEXT, - writing_style TEXT, - themes TEXT, - symbolism TEXT, - motifs TEXT, - narrative_voice TEXT, - pacing TEXT, - intended_audience TEXT, - key_messages TEXT, - last_update INTEGER DEFAULT 0, - PRIMARY KEY (user_id, book_id), - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - - // Book Incidents - db.exec(` - CREATE TABLE IF NOT EXISTS book_incidents ( - incident_id TEXT PRIMARY KEY, - author_id TEXT NOT NULL, - book_id TEXT NOT NULL, - title TEXT NOT NULL, - hashed_title TEXT NOT NULL, - summary TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - - // Book Issues - db.exec(` - CREATE TABLE IF NOT EXISTS book_issues ( - issue_id TEXT PRIMARY KEY, - author_id TEXT NOT NULL, - book_id TEXT NOT NULL, - name TEXT NOT NULL, - hashed_issue_name TEXT NOT NULL, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - - // Book Location - db.exec(` - CREATE TABLE IF NOT EXISTS book_location ( - loc_id TEXT PRIMARY KEY, - book_id TEXT NOT NULL, - user_id TEXT NOT NULL, - loc_name TEXT NOT NULL, - loc_original_name TEXT NOT NULL, - series_location_id TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE, - FOREIGN KEY (series_location_id) REFERENCES series_locations(loc_id) ON DELETE SET NULL - ); - `); - - // Book Plot Points - db.exec(` - CREATE TABLE IF NOT EXISTS book_plot_points ( - plot_point_id TEXT PRIMARY KEY, - title TEXT NOT NULL, - hashed_title TEXT NOT NULL, - summary TEXT, - linked_incident_id TEXT, - author_id TEXT NOT NULL, - book_id TEXT NOT NULL, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - - // Book World - db.exec(` - CREATE TABLE IF NOT EXISTS book_world ( - world_id TEXT PRIMARY KEY, - name TEXT NOT NULL, - hashed_name TEXT NOT NULL, - author_id TEXT NOT NULL, - book_id TEXT NOT NULL, - history TEXT, - politics TEXT, - economy TEXT, - religion TEXT, - languages TEXT, - series_world_id TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE, - FOREIGN KEY (series_world_id) REFERENCES series_worlds(world_id) ON DELETE SET NULL - ); - `); - - // Book World Elements - db.exec(` - CREATE TABLE IF NOT EXISTS book_world_elements ( - element_id TEXT PRIMARY KEY, - world_id TEXT NOT NULL, - user_id TEXT NOT NULL, - element_type INTEGER NOT NULL, - name TEXT NOT NULL, - original_name TEXT NOT NULL, - description TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (world_id) REFERENCES book_world(world_id) ON DELETE CASCADE - ); - `); - - // Erit Books - db.exec(` - CREATE TABLE IF NOT EXISTS erit_books ( - book_id TEXT PRIMARY KEY, - type TEXT NOT NULL, - author_id TEXT NOT NULL, - title TEXT NOT NULL, - hashed_title TEXT NOT NULL, - sub_title TEXT, - hashed_sub_title TEXT, - summary TEXT, - serie_id INTEGER, - desired_release_date TEXT, - desired_word_count INTEGER, - words_count INTEGER, - cover_image TEXT, - last_update INTEGER DEFAULT 0 - ); - `); - - // Erit Book Series - db.exec(` - CREATE TABLE IF NOT EXISTS erit_book_series ( - serie_id INTEGER PRIMARY KEY, - name TEXT NOT NULL, - author_id INTEGER NOT NULL - ); - `); - - // Erit Editor Settings - db.exec(` - CREATE TABLE IF NOT EXISTS erit_editor ( - user_id TEXT, - type TEXT NOT NULL, - text_size INTEGER NOT NULL, - text_intent INTEGER NOT NULL, - interline TEXT NOT NULL, - paper_width INTEGER NOT NULL, - theme TEXT NOT NULL, - focus INTEGER NOT NULL - ); - `); - - // Erit Users - db.exec(` - CREATE TABLE IF NOT EXISTS erit_users ( - user_id TEXT PRIMARY KEY, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, - username TEXT NOT NULL, - email TEXT NOT NULL, - origin_email TEXT NOT NULL, - origin_username TEXT NOT NULL, - author_name TEXT, - origin_author_name TEXT, - plateform TEXT NOT NULL, - social_id TEXT, - user_group INTEGER NOT NULL DEFAULT 4, - password TEXT, - term_accepted INTEGER NOT NULL DEFAULT 0, - verify_code TEXT, - reg_date INTEGER NOT NULL, - account_verified INTEGER NOT NULL DEFAULT 0, - erite_points INTEGER NOT NULL DEFAULT 100, - stripe_customer_id TEXT, - credits_balance REAL DEFAULT 0 - ); - `); - - // Location Element - db.exec(` - CREATE TABLE IF NOT EXISTS location_element ( - element_id TEXT PRIMARY KEY, - location TEXT NOT NULL, - user_id TEXT NOT NULL, - element_name TEXT NOT NULL, - original_name TEXT NOT NULL, - element_description TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (location) REFERENCES book_location(loc_id) ON DELETE CASCADE - ); - `); - - // Location Sub Element - db.exec(` - CREATE TABLE IF NOT EXISTS location_sub_element ( - sub_element_id TEXT PRIMARY KEY, - element_id TEXT NOT NULL, - user_id TEXT NOT NULL, - sub_elem_name TEXT NOT NULL, - original_name TEXT NOT NULL, - sub_elem_description TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (element_id) REFERENCES location_element(element_id) ON DELETE CASCADE - ); - `); - - // User Keys - db.exec(` - CREATE TABLE IF NOT EXISTS user_keys ( - user_id TEXT NOT NULL, - brand TEXT NOT NULL, - key TEXT NOT NULL, - actif INTEGER NOT NULL DEFAULT 1, - FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE - ); - `); - - // User Last Chapter - db.exec(` - CREATE TABLE IF NOT EXISTS user_last_chapter ( - user_id TEXT NOT NULL, - book_id TEXT NOT NULL, - chapter_id TEXT NOT NULL, - version INTEGER NOT NULL, - PRIMARY KEY (user_id, book_id), - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE, - FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE - ); - `); - - // Book Tools - db.exec(` - CREATE TABLE IF NOT EXISTS book_tools ( - book_id TEXT NOT NULL, - user_id TEXT NOT NULL, - characters_enabled INTEGER NOT NULL DEFAULT 0, - worlds_enabled INTEGER NOT NULL DEFAULT 0, - locations_enabled INTEGER NOT NULL DEFAULT 0, - spells_enabled INTEGER NOT NULL DEFAULT 0, - last_update INTEGER DEFAULT 0, - UNIQUE (book_id, user_id), - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - - // Book Spell Tags - db.exec(` - CREATE TABLE IF NOT EXISTS book_spell_tags ( - tag_id TEXT PRIMARY KEY, - book_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - name_hash TEXT NOT NULL, - color TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - - // Book Spells - db.exec(` - CREATE TABLE IF NOT EXISTS book_spells ( - spell_id TEXT PRIMARY KEY, - book_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - name_hash TEXT NOT NULL, - description TEXT, - appearance TEXT, - tags TEXT, - power_level TEXT, - components TEXT, - limitations TEXT, - notes TEXT, - series_spell_id TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE, - FOREIGN KEY (series_spell_id) REFERENCES series_spells(spell_id) ON DELETE SET NULL - ); - `); - - // ============================================================================= - // SERIES TABLES - // ============================================================================= - - // Book Series (main series table) - db.exec(` - CREATE TABLE IF NOT EXISTS book_series ( - series_id TEXT PRIMARY KEY, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - hashed_name TEXT NOT NULL, - description TEXT, - cover_image TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE - ); - `); - - // Series Books (link series to books with order) - db.exec(` - CREATE TABLE IF NOT EXISTS series_books ( - series_id TEXT NOT NULL, - book_id TEXT NOT NULL, - book_order INTEGER NOT NULL DEFAULT 1, - last_update INTEGER DEFAULT 0, - PRIMARY KEY (series_id, book_id), - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE, - FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE - ); - `); - - // Series Characters - db.exec(` - CREATE TABLE IF NOT EXISTS series_characters ( - character_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - first_name TEXT NOT NULL, - last_name TEXT, - nickname TEXT, - age TEXT, - gender TEXT, - species TEXT, - nationality TEXT, - status TEXT, - category TEXT NOT NULL, - title TEXT, - image TEXT, - role TEXT, - biography TEXT, - history TEXT, - speech_pattern TEXT, - catchphrase TEXT, - residence TEXT, - notes TEXT, - color TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - ); - `); - - // Series Characters Attributes - db.exec(` - CREATE TABLE IF NOT EXISTS series_characters_attributes ( - attr_id TEXT PRIMARY KEY, - character_id TEXT NOT NULL, - user_id TEXT NOT NULL, - attribute_name TEXT NOT NULL, - attribute_value TEXT NOT NULL, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (character_id) REFERENCES series_characters(character_id) ON DELETE CASCADE - ); - `); - - // Series Worlds - db.exec(` - CREATE TABLE IF NOT EXISTS series_worlds ( - world_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - hashed_name TEXT NOT NULL, - history TEXT, - politics TEXT, - economy TEXT, - religion TEXT, - languages TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - ); - `); - - // Series World Elements - db.exec(` - CREATE TABLE IF NOT EXISTS series_world_elements ( - element_id TEXT PRIMARY KEY, - world_id TEXT NOT NULL, - user_id TEXT NOT NULL, - element_type INTEGER NOT NULL, - name TEXT NOT NULL, - original_name TEXT NOT NULL, - description TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (world_id) REFERENCES series_worlds(world_id) ON DELETE CASCADE - ); - `); - - // Series Locations - db.exec(` - CREATE TABLE IF NOT EXISTS series_locations ( - loc_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - loc_name TEXT NOT NULL, - loc_original_name TEXT NOT NULL, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - ); - `); - - // Series Location Elements - db.exec(` - CREATE TABLE IF NOT EXISTS series_location_elements ( - element_id TEXT PRIMARY KEY, - location_id TEXT NOT NULL, - user_id TEXT NOT NULL, - element_name TEXT NOT NULL, - original_name TEXT NOT NULL, - element_description TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (location_id) REFERENCES series_locations(loc_id) ON DELETE CASCADE - ); - `); - - // Series Location Sub Elements - db.exec(` - CREATE TABLE IF NOT EXISTS series_location_sub_elements ( - sub_element_id TEXT PRIMARY KEY, - element_id TEXT NOT NULL, - user_id TEXT NOT NULL, - sub_elem_name TEXT NOT NULL, - original_name TEXT NOT NULL, - sub_elem_description TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (element_id) REFERENCES series_location_elements(element_id) ON DELETE CASCADE - ); - `); - - // Series Spells - db.exec(` - CREATE TABLE IF NOT EXISTS series_spells ( - spell_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - name_hash TEXT NOT NULL, - description TEXT, - appearance TEXT, - tags TEXT, - power_level TEXT, - components TEXT, - limitations TEXT, - notes TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - ); - `); - - // Series Spell Tags - db.exec(` - CREATE TABLE IF NOT EXISTS series_spell_tags ( - tag_id TEXT PRIMARY KEY, - series_id TEXT NOT NULL, - user_id TEXT NOT NULL, - name TEXT NOT NULL, - hashed_name TEXT NOT NULL, - color TEXT, - last_update INTEGER DEFAULT 0, - FOREIGN KEY (series_id) REFERENCES book_series(series_id) ON DELETE CASCADE - ); - `); - - // Removed Items (sync deletion tracking) - db.exec(` - CREATE TABLE IF NOT EXISTS removed_items ( - removal_id TEXT PRIMARY KEY, - table_name TEXT NOT NULL, - entity_id TEXT NOT NULL, - book_id TEXT, - user_id TEXT NOT NULL, - deleted_at INTEGER NOT NULL, - removed_time INTEGER NOT NULL DEFAULT 0 - ) - `); - - // Create indexes for better performance - createIndexes(db); -} - -/** - * Create indexes for frequently queried columns - */ -function createIndexes(db: Database): void { - db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_conversations_book ON ai_conversations(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_conversations_user ON ai_conversations(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_messages_conversation ON ai_messages_history(conversation_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_chapters_book ON book_chapters(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_chapter_content_chapter ON book_chapter_content(chapter_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_characters_book ON book_characters(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_character_attrs_character ON book_characters_attributes(character_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_world_book ON book_world(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_world_elements_world ON book_world_elements(world_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_book ON book_tools(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_user ON book_tools(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_book ON book_spell_tags(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_spell_tags_user ON book_spell_tags(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_book ON book_spells(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_spells_user ON book_spells(user_id)`); - - // Series tables indexes - db.exec(`CREATE INDEX IF NOT EXISTS idx_book_series_user ON book_series(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_books_book ON series_books(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_series ON series_characters(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_characters_user ON series_characters(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_character ON series_characters_attributes(character_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_char_attrs_user ON series_characters_attributes(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_series ON series_worlds(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_worlds_user ON series_worlds(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_world ON series_world_elements(world_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_world_elements_user ON series_world_elements(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_series ON series_locations(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_locations_user ON series_locations(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_location ON series_location_elements(location_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_elements_user ON series_location_elements(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_element ON series_location_sub_elements(element_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_loc_sub_elements_user ON series_location_sub_elements(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_series ON series_spells(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spells_user ON series_spells(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_series ON series_spell_tags(series_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_series_spell_tags_user ON series_spell_tags(user_id)`); - // Removed items indexes - db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_user ON removed_items(user_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_book ON removed_items(book_id)`); - db.exec(`CREATE INDEX IF NOT EXISTS idx_removed_items_deleted_at ON removed_items(deleted_at)`); - db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_removed_items_entity ON removed_items(table_name, entity_id)`); -} - -/** - * Drop all tables (for testing/reset) - */ -export function dropAllTables(db: Database): void { - const tables = db.all(` - SELECT name FROM sqlite_master - WHERE type='table' AND name NOT LIKE 'sqlite_%' - `) as { name: string }[]; - - db.exec('PRAGMA foreign_keys = OFF'); - - for (const row of tables) { - db.exec(`DROP TABLE IF EXISTS ${row.name}`); - } - - db.exec('PRAGMA foreign_keys = ON'); -} diff --git a/electron/ipc/book.ipc.ts b/electron/ipc/book.ipc.ts deleted file mode 100644 index da1eb7a..0000000 --- a/electron/ipc/book.ipc.ts +++ /dev/null @@ -1,501 +0,0 @@ -import { ipcMain, dialog, BrowserWindow } from 'electron'; -import { writeFile } from 'fs/promises'; -import { createHandler } from '../database/LocalSystem.js'; -import Book, {BookSyncCompare, CompleteBook, CompleteBookData, SyncedBook} from '../database/models/Book.js'; -import type { BookProps } from '../database/models/Book.js'; -import Chapter, {ChapterExportInfo} from '../database/models/Chapter.js'; -import type { ChapterProps } from '../database/models/Chapter.js'; -import {ChapterSelectionParam} from "../database/repositories/chapter.repository.js"; -import Act, {ActProps} from "../database/models/Act.js"; -import Issue, {IssueProps} from "../database/models/Issue.js"; -import Sync from "../database/models/Sync.js"; -import Download from "../database/models/Download.js"; -import Upload from "../database/models/Upload.js"; -import GuideLine, {GuideLineAI} from "../database/models/GuideLine.js"; -import Incident from "../database/models/Incident.js"; -import PlotPoint from "../database/models/PlotPoint.js"; -import World, {WorldListResponse, WorldProps} from "../database/models/World.js"; -import Export, {ExportResult} from "../database/models/Export.js"; - -interface UpdateBookBasicData { - title: string; - subTitle: string; - summary: string; - publicationDate: string; - wordCount: number; - bookId: string; -} - -interface UpdateGuideLineData { - bookId: string; - tone: string | null; - atmosphere: string | null; - writingStyle: string | null; - themes: string | null; - symbolism: string | null; - motifs: string | null; - narrativeVoice: string | null; - pacing: string | null; - keyMessages: string | null; - intendedAudience: string | null; -} - -interface StoryData { - acts: ActProps[]; - issues: IssueProps[]; - mainChapter: ChapterProps[]; -} - -interface UpdateStoryData { - bookId: string; - acts: ActProps[]; - mainChapters: ChapterProps[]; -} - -interface CreateBookData { - title: string; - subTitle: string | null; - summary: string | null; - type: string; - serieId: number | null; - desiredReleaseDate: string | null; - desiredWordCount: number | null; -} - -interface AddIncidentData { - bookId: string; - name: string; - incidentId?: string; -} - -interface AddPlotPointData { - bookId: string; - name: string; - incidentId: string; - plotPointId?: string; -} - -interface AddIssueData { - bookId: string; - name: string; - issueId?: string; -} - -interface AddWorldData { - bookId: string; - worldName: string; - id?: string; - seriesWorldId?: string | null; -} - -interface AddWorldElementData { - worldId: string; - elementName: string; - elementType: number; - id?: string; -} - -interface SetAIGuideLineData { - bookId: string; - narrativeType: number; - dialogueType: number; - plotSummary: string; - toneAtmosphere: string; - verbTense: number; - language: number; - themes: string; -} - -interface GetGuidelineData { - id: string; -} - -interface UpdateWorldData { - bookId: string; - world: WorldProps; -} - -interface UpdateBookToolData { - bookId: string; - toolName: 'characters' | 'worlds' | 'locations' | 'spells'; - enabled: boolean; -} - -// GET /books - Get all books -ipcMain.handle('db:book:books', createHandler( - async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise { - return await Book.getBooks(userId, lang); - } - ) -); - -// GET /books/synced - Get all synced books -ipcMain.handle('db:books:synced', createHandler( - async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise { - return await Sync.getSyncedBooks(userId, lang); - }) -); - -// POST /book/sync/save - Save complete book -ipcMain.handle('db:book:syncSave', createHandler( - async function(userId: string, data: CompleteBook, lang: 'fr' | 'en'):Promise { - return await Download.saveCompleteBook(userId, data, lang); - }) -); - -// GET /book/:id - Get single book -ipcMain.handle('db:book:bookBasicInformation', createHandler( - async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise { - return await Book.getBook(userId, bookId, lang); - } - ) -); - -// GET -ipcMain.handle('db:book:uploadToServer', createHandler( - async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise { - return await Upload.uploadBookForSync(userId, bookId, lang); - } - ) -); - -// POST /book/basic-information - Update book basic info -ipcMain.handle('db:book:updateBasicInformation', createHandler( - function(userId: string, data: UpdateBookBasicData, lang: 'fr' | 'en') { - return Book.updateBookBasicInformation(userId, data.title, data.subTitle, data.summary, data.publicationDate, data.wordCount, data.bookId, lang); - } - ) -); - -// GET /book/sync/to-client - Get book data to sync to client -ipcMain.handle('db:book:sync:toServer', createHandler( - async function(userId: string, data:BookSyncCompare, lang: 'fr' | 'en'):Promise { - return await Sync.getCompleteSyncBook(userId, data, lang); - } - ) -); - -// GET /book/sync/from-server - Get book data to sync from server -ipcMain.handle('db:book:sync:toClient', createHandler( - async function(userId: string, data:CompleteBook, lang: 'fr' | 'en'):Promise { - return await Sync.syncBookFromServerToClient(userId, data, lang); - } - ) -); - -// GET /book/guide-line - Get guideline -ipcMain.handle('db:book:guideline:get', - createHandler(async function(userId: string, data: GetGuidelineData, lang: 'fr' | 'en') { - return await GuideLine.getGuideLine(userId, data.id, lang); - } - ) -); - -// POST /book/guide-line - Update guideline -ipcMain.handle('db:book:guideline:update', createHandler( - async function(userId: string, data: UpdateGuideLineData, lang: 'fr' | 'en') { - return await GuideLine.updateGuideLine( - userId, - data.bookId, - data.tone, - data.atmosphere, - data.writingStyle, - data.themes, - data.symbolism, - data.motifs, - data.narrativeVoice, - data.pacing, - data.keyMessages, - data.intendedAudience, - lang - ); - } - ) -); - -// GET /book/story - Get story data (acts + issues + mainChapter) -interface GetStoryData { - bookid: string; -} -ipcMain.handle('db:book:story:get', createHandler( - async function(userId: string, data: GetStoryData, lang: 'fr' | 'en'):Promise { - const acts:ActProps[] = await Act.getActsData(userId, data.bookid, lang); - const issues:IssueProps[] = await Issue.getIssuesFromBook(userId, data.bookid, lang); - const mainChapter:ChapterProps[] = Chapter.getAllChaptersFromABook(userId, data.bookid, lang); - return { - acts, - issues, - mainChapter - }; - } - ) -); - -// POST /book/story - Update story (acts + mainChapters) -ipcMain.handle('db:book:story:update', createHandler( - function(userId: string, data: UpdateStoryData, lang: 'fr' | 'en'):boolean { - return Act.updateStory(userId, data.bookId, data.acts, data.mainChapters, lang); - } - ) -); - -// POST /book/add - Create new book -ipcMain.handle('db:book:create', createHandler( - function(userId: string, data: CreateBookData, lang: 'fr' | 'en') { - return Book.addBook( - null, - userId, - data.title, - data.subTitle || '', - data.summary || '', - data.type, - data.serieId || 0, - data.desiredReleaseDate || '', - data.desiredWordCount || 0, - lang - ); - } - ) -); - - -// POST /book/incident/new - Add incident -ipcMain.handle( - 'db:book:incident:add', - createHandler( - function(userId: string, data: AddIncidentData, lang: 'fr' | 'en') { - return Incident.addNewIncident(userId, data.bookId, data.name, lang, data.incidentId); - } - ) -); - -// DELETE /book/incident/remove - Remove incident -interface RemoveIncidentData { - bookId: string; - incidentId: string; - deletedAt: number; -} - -ipcMain.handle('db:book:incident:remove', createHandler( - function(userId: string, data: RemoveIncidentData, lang: 'fr' | 'en') { - return Incident.removeIncident(userId, data.bookId, data.incidentId, data.deletedAt, lang); - } - ) -); - -// POST /book/plot/new - Add plot point -ipcMain.handle('db:book:plot:add', createHandler( - function(userId: string, data: AddPlotPointData, lang: 'fr' | 'en'):string { - return PlotPoint.addNewPlotPoint( - userId, - data.bookId, - data.incidentId, - data.name, - lang, - data.plotPointId - ); - } - ) -); - -// DELETE /book/plot/remove - Remove plot point -interface RemovePlotData { - plotId: string; - bookId: string; - deletedAt: number; -} -ipcMain.handle( - 'db:book:plot:remove', - createHandler( - function(userId: string, data: RemovePlotData, lang: 'fr' | 'en') { - return PlotPoint.removePlotPoint(userId, data.bookId, data.plotId, data.deletedAt, lang); - } - ) -); - -// POST /book/issue/add - Add issue -ipcMain.handle('db:book:issue:add', createHandler( - function(userId: string, data: AddIssueData, lang: 'fr' | 'en') { - return Issue.addNewIssue(userId, data.bookId, data.name, lang, data.issueId); - } - ) -); - -// DELETE /book/issue/remove - Remove issue -interface RemoveIssueData { - bookId: string; - issueId: string; - deletedAt: number; -} -ipcMain.handle('db:book:issue:remove', createHandler( - function(userId: string, data: RemoveIssueData, lang: 'fr' | 'en') { - return Issue.removeIssue(userId, data.bookId, data.issueId, data.deletedAt, lang); - } - ) -); - -// GET /book/worlds - Get worlds for book -interface GetWorldsData { - bookid: string; -} -ipcMain.handle('db:book:worlds:get', createHandler( - function(userId: string, data: GetWorldsData, lang: 'fr' | 'en'): WorldListResponse { - return World.getWorlds(userId, data.bookid, lang); - } - ) -); - -// POST /book/world/add - Add world -ipcMain.handle('db:book:world:add', createHandler( - function(userId: string, data: AddWorldData, lang: 'fr' | 'en') { - return World.addNewWorld(userId, data.bookId, data.worldName, lang, data.id, data.seriesWorldId || null); - } - ) -); - -// POST /book/world/element/add - Add element to world -ipcMain.handle('db:book:world:element:add', createHandler( - function(userId: string, data: AddWorldElementData, lang: 'fr' | 'en') { - return World.addNewElementToWorld( - userId, - data.worldId, - data.elementName, - data.elementType.toString(), - lang, - data.id - ); - } - ) -); -// DELETE /book/world/element/delete - Remove element from world -interface RemoveWorldElementData { - elementId: string; - bookId: string; - deletedAt: number; -} -ipcMain.handle('db:book:world:element:remove', createHandler( - function(userId: string, data: RemoveWorldElementData, lang: 'fr' | 'en') { - return World.removeElementFromWorld(userId, data.bookId, data.elementId, data.deletedAt, lang); - } - ) -); - -// DELETE /book/delete - Delete book -interface DeleteBookData { - id: string; - deletedAt: number; -} -ipcMain.handle('db:book:delete', createHandler( - function(userId: string, data: DeleteBookData, lang: 'fr' | 'en') { - return Book.removeBook(userId, data.id, data.deletedAt, lang); - } - ) -); - -// GET /book/ai/guideline - Get AI guideline -interface GetAIGuidelineData { - id: string; -} -ipcMain.handle('db:book:guideline:ai:get', createHandler( - function(userId: string, data: GetAIGuidelineData, lang: 'fr' | 'en') { - return GuideLine.getGuideLineAI(userId, data.id, lang); - } - ) -); - -// POST /book/ai/guideline (set) - Set AI guideline -ipcMain.handle('db:book:guideline:ai:set', createHandler( - function(userId: string, data: SetAIGuideLineData, lang: 'fr' | 'en') { - return GuideLine.setAIGuideLine( - userId, - data.bookId, - data.narrativeType, - data.dialogueType, - data.plotSummary, - data.toneAtmosphere, - data.verbTense, - data.language, - data.themes, - lang - ); - } - ) -); - -// PUT /book/world/update - Update world -ipcMain.handle('db:book:world:update', createHandler( - function(userId: string, data: UpdateWorldData, lang: 'fr' | 'en') { - return World.updateWorld(userId, data.world, lang); - } - ) -); - -// PATCH /book/tool-setting - Update book tool setting -ipcMain.handle('db:book:tool:update', createHandler( - function(userId: string, data: UpdateBookToolData, lang: 'fr' | 'en') { - return Book.updateBookToolSetting(userId, data.bookId, data.toolName, data.enabled, lang); - } - ) -); - -// GET /book/export/info - Get chapters export info (available versions) -interface ExportInfoData { - bookId: string; -} -ipcMain.handle('db:book:export:info', createHandler( - function(userId: string, data: ExportInfoData, lang: 'fr' | 'en'): ChapterExportInfo[] { - return Chapter.getChaptersExportInfo(userId, data.bookId, lang); - } - ) -); - -// POST /book/export - Export book to file (EPUB/PDF/DOCX) -type ExportFormat = 'epub' | 'pdf' | 'docx'; - -interface ExportRequestData { - bookId: string; - format: ExportFormat; - selections: ChapterSelectionParam[] | null; -} - -const formatExtensions: Record = { - epub: {ext: 'epub', filterName: 'EPUB'}, - pdf: {ext: 'pdf', filterName: 'PDF'}, - docx: {ext: 'docx', filterName: 'Word Document'} -}; - -ipcMain.handle('db:book:export', createHandler( - async function(userId: string, data: ExportRequestData, lang: 'fr' | 'en'): Promise { - const bookData: CompleteBookData = Chapter.getCompleteBookDataWithSelections(userId, data.bookId, data.selections, lang); - - let result: ExportResult; - switch (data.format) { - case 'epub': - result = await Export.transformToEpub(bookData); - break; - case 'pdf': - result = await Export.transformToPDF(bookData); - break; - case 'docx': - result = await Export.transformToDOCX(bookData); - break; - default: - throw new Error(lang === 'fr' ? 'Format non supporté.' : 'Unsupported format.'); - } - - const formatInfo = formatExtensions[data.format]; - const focusedWindow: BrowserWindow | null = BrowserWindow.getFocusedWindow(); - const dialogResult = await dialog.showSaveDialog(focusedWindow!, { - defaultPath: result.fileName, - filters: [{name: formatInfo.filterName, extensions: [formatInfo.ext]}] - }); - - if (dialogResult.canceled || !dialogResult.filePath) { - return false; - } - - await writeFile(dialogResult.filePath, result.buffer); - return true; - } - ) -); diff --git a/electron/ipc/chapter.ipc.ts b/electron/ipc/chapter.ipc.ts deleted file mode 100644 index 9de5e34..0000000 --- a/electron/ipc/chapter.ipc.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { ipcMain } from 'electron'; -import { createHandler } from '../database/LocalSystem.js'; -import Chapter from '../database/models/Chapter.js'; -import type { ChapterProps, CompanionContent } from '../database/models/Chapter.js'; -import type { ActStory } from '../database/models/Act.js'; - -interface GetWholeChapterData { - id: string; - version: number; - bookid: string; -} - -interface SaveChapterContentData { - chapterId: string; - version: number; - content: JSON; - totalWordCount: number; - currentTime: number; -} - -interface AddChapterData { - bookId: string; - title: string; - chapterOrder: number; - chapterId?: string; -} - -interface UpdateChapterData { - chapterId: string; - title: string; - chapterOrder: number; -} - -interface AddChapterInformationData { - chapterId: string; - actId: number; - bookId: string; - plotId: string | null; - incidentId: string | null; - chapterInfoId?: string; -} - -interface GetChapterContentData { - chapterId: string; - version: number; -} - -// GET /book/chapters - Get all chapters from a book -ipcMain.handle('db:book:chapters', createHandler( - function(userId: string, bookId: string, lang: 'fr' | 'en'): ChapterProps[] { - return Chapter.getAllChaptersFromABook(userId, bookId, lang); - } - ) -); - -// GET /chapter/whole - Get whole chapter -ipcMain.handle('db:chapter:whole', createHandler( - function(userId: string, data: GetWholeChapterData, lang: 'fr' | 'en'): ChapterProps { - return Chapter.getWholeChapter(userId, data.id, data.version, data.bookid, lang); - } - ) -); - -// GET /chapter/:id/story - Get chapter story -ipcMain.handle('db:chapter:story', createHandler( - function(userId: string, chapterId: string, lang: 'fr' | 'en'): ActStory[] { - return Chapter.getChapterStory(userId, chapterId, lang); - } - ) -); - -// GET /chapter/content/companion - Get companion content -ipcMain.handle('db:chapter:content:companion', createHandler( - function(userId: string, data: GetChapterContentData, lang: 'fr' | 'en'): CompanionContent { - return Chapter.getCompanionContent(userId, data.chapterId, data.version, lang); - } - ) -); - -// GET /chapter/content - Get chapter content by version -ipcMain.handle('db:chapter:content:get', createHandler( - function(userId: string, data: GetChapterContentData, lang: 'fr' | 'en'): string { - return Chapter.getChapterContentByVersion(userId, data.chapterId, data.version, lang); - } - ) -); - -// POST /chapter/content - Save chapter content -ipcMain.handle('db:chapter:content:save', createHandler( - function(userId: string, data: SaveChapterContentData, lang: 'fr' | 'en'): boolean { - return Chapter.saveChapterContent( - userId, - data.chapterId, - data.version, - data.content, - data.totalWordCount, - data.currentTime, - lang - ); - } - ) -); - -// GET /chapter/last-chapter - Get last chapter -ipcMain.handle('db:chapter:last', createHandler( - function(userId: string, bookId: string, lang: 'fr' | 'en'): ChapterProps | null { - return Chapter.getLastChapter(userId, bookId, lang); - } - ) -); - -// POST /chapter/add - Add new chapter -ipcMain.handle('db:chapter:add', createHandler( - function(userId: string, data: AddChapterData, lang: 'fr' | 'en'): string { - return Chapter.addChapter(userId, data.bookId, data.title, 0, data.chapterOrder, lang, data.chapterId); - } - ) -); - -// DELETE /chapter/remove - Remove chapter -interface RemoveChapterData { - chapterId: string; - bookId: string; - deletedAt: number; -} -ipcMain.handle('db:chapter:remove', createHandler( - function(userId: string, data: RemoveChapterData, lang: 'fr' | 'en'): boolean { - return Chapter.removeChapter(userId, data.bookId, data.chapterId, data.deletedAt, lang); - } - ) -); - -// POST /chapter/update - Update chapter -ipcMain.handle('db:chapter:update', createHandler( - function(userId: string, data: UpdateChapterData, lang: 'fr' | 'en'): boolean { - return Chapter.updateChapter(userId, data.chapterId, data.title, data.chapterOrder, lang); - } - ) -); - -// POST /chapter/resume/add - Add chapter information -ipcMain.handle('db:chapter:information:add', createHandler( - function(userId: string, data: AddChapterInformationData, lang: 'fr' | 'en'): string { - return Chapter.addChapterInformation( - userId, - data.chapterId, - data.actId, - data.bookId, - data.plotId, - data.incidentId, - lang, - data.chapterInfoId - ); - } - ) -); - -// DELETE /chapter/resume/remove - Remove chapter information -interface RemoveChapterInfoData { - chapterInfoId: string; - bookId: string; - deletedAt: number; -} -ipcMain.handle('db:chapter:information:remove', createHandler( - function(userId: string, data: RemoveChapterInfoData, lang: 'fr' | 'en'): boolean { - return Chapter.removeChapterInformation(userId, data.bookId, data.chapterInfoId, data.deletedAt, lang); - } - ) -); diff --git a/electron/ipc/character.ipc.ts b/electron/ipc/character.ipc.ts deleted file mode 100644 index 5534ba8..0000000 --- a/electron/ipc/character.ipc.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ipcMain } from 'electron'; -import { createHandler } from '../database/LocalSystem.js'; -import Character, {CharacterListResponse} from '../database/models/Character.js'; -import type { CharacterPropsPost, CharacterAttribute } from '../database/models/Character.js'; - -interface AddCharacterData { - character: CharacterPropsPost; - bookId: string; - id?: string; -} - -interface AddAttributeData { - characterId: string; - type: string; - name: string; - id?: string; -} - -// GET /character/list - Get character list -interface GetCharacterListData { - bookid: string; -} -ipcMain.handle('db:character:list', createHandler( - function(userId: string, data: GetCharacterListData, lang: 'fr' | 'en'): CharacterListResponse { - return Character.getCharacterList(userId, data.bookid, lang); - } - ) -); - -// GET /character/attribute - Get character attributes -interface GetCharacterAttributesData { - characterId: string; -} -ipcMain.handle('db:character:attributes', createHandler( - function(userId: string, data: GetCharacterAttributesData, lang: 'fr' | 'en'): CharacterAttribute[] { - return Character.getAttributes(data.characterId, userId, lang); - } - ) -); - -// POST /character/add - Add new character -ipcMain.handle('db:character:create', createHandler( - function(userId: string, data: AddCharacterData, lang: 'fr' | 'en'): string { - return Character.addNewCharacter(userId, data.character, data.bookId, lang, data.id); - } - ) -); - -// POST /character/attribute/add - Add attribute to character -ipcMain.handle('db:character:attribute:add', createHandler( - function(userId: string, data: AddAttributeData, lang: 'fr' | 'en'): string { - return Character.addNewAttribute(data.characterId, userId, data.type, data.name, lang, data.id); - } - ) -); - -// DELETE /character/attribute/delete - Delete character attribute -interface DeleteAttributeData { - attributeId: string; - bookId: string; - deletedAt: number; -} -ipcMain.handle('db:character:attribute:delete', createHandler( - function(userId: string, data: DeleteAttributeData, lang: 'fr' | 'en'): boolean { - return Character.deleteAttribute(userId, data.bookId, data.attributeId, data.deletedAt, lang); - } - ) -); - -// POST /character/update - Update character -interface UpdateCharacterData { - character: CharacterPropsPost; -} -ipcMain.handle('db:character:update', createHandler( - function(userId: string, data: UpdateCharacterData, lang: 'fr' | 'en'): boolean { - return Character.updateCharacter(userId, data.character, lang); - } - ) -); - -// DELETE /character/delete - Delete character -interface DeleteCharacterData { - characterId: string; - bookId: string; - deletedAt: number; -} -ipcMain.handle('db:character:delete', createHandler( - function(userId: string, data: DeleteCharacterData, lang: 'fr' | 'en'): boolean { - return Character.deleteCharacter(userId, data.bookId, data.characterId, data.deletedAt, lang); - } - ) -); \ No newline at end of file diff --git a/electron/ipc/location.ipc.ts b/electron/ipc/location.ipc.ts deleted file mode 100644 index 8824ceb..0000000 --- a/electron/ipc/location.ipc.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { ipcMain } from 'electron'; -import { createHandler } from '../database/LocalSystem.js'; -import Location, {LocationListResponse} from '../database/models/Location.js'; -import type { LocationProps } from '../database/models/Location.js'; - -interface UpdateLocationResponse { - valid: boolean; - message: string; -} - -interface AddLocationSectionData { - locationName: string; - bookId: string; - id?: string; - seriesLocationId?: string | null; -} - -interface AddLocationElementData { - locationId: string; - elementName: string; - id?: string; -} - -interface AddLocationSubElementData { - elementId: string; - subElementName: string; - id?: string; -} - -interface UpdateLocationData { - locations: LocationProps[]; -} - -// GET /location/all - Get all locations -interface GetAllLocationsData { - bookid: string; -} -ipcMain.handle('db:location:all', createHandler( - function(userId: string, data: GetAllLocationsData, lang: 'fr' | 'en'): LocationListResponse { - return Location.getAllLocations(userId, data.bookid, lang); - } - ) -); - -// POST /location/section/add - Add location section -ipcMain.handle('db:location:section:add', createHandler( - function(userId: string, data: AddLocationSectionData, lang: 'fr' | 'en'): string { - return Location.addLocationSection(userId, data.locationName, data.bookId, lang, data.id, data.seriesLocationId || null); - } - ) -); - -// POST /location/element/add - Add location element -ipcMain.handle('db:location:element:add', createHandler( - function(userId: string, data: AddLocationElementData, lang: 'fr' | 'en'): string { - return Location.addLocationElement(userId, data.locationId, data.elementName, lang, data.id); - } - ) -); - -// POST /location/sub-element/add - Add location sub-element -ipcMain.handle('db:location:subelement:add', createHandler( - function(userId: string, data: AddLocationSubElementData, lang: 'fr' | 'en'): string { - return Location.addLocationSubElement(userId, data.elementId, data.subElementName, lang, data.id); - } - ) -); - -// POST /location/update - Update location section -ipcMain.handle('db:location:update', createHandler( - function(userId: string, data: UpdateLocationData, lang: 'fr' | 'en'): UpdateLocationResponse { - return Location.updateLocationSection(userId, data.locations, lang); - } - ) -); - -// POST /location/section/update - Update location section with series link -interface UpdateSectionWithSeriesLinkData { - sectionId: string; - sectionName?: string; - seriesLocationId?: string | null; -} -ipcMain.handle('db:location:section:update', createHandler( - function(userId: string, data: UpdateSectionWithSeriesLinkData, lang: 'fr' | 'en'): boolean { - return Location.updateSectionWithSeriesLink(userId, data.sectionId, data.sectionName, data.seriesLocationId, lang); - } - ) -); - -// DELETE /location/delete - Delete location section -interface DeleteLocationData { - locationId: string; - bookId: string; - deletedAt: number; -} -ipcMain.handle('db:location:delete', createHandler( - function(userId: string, data: DeleteLocationData, lang: 'fr' | 'en'): boolean { - return Location.deleteLocationSection(userId, data.bookId, data.locationId, data.deletedAt, lang); - } - ) -); - -// DELETE /location/element/delete - Delete location element -interface DeleteLocationElementData { - elementId: string; - bookId: string; - deletedAt: number; -} -ipcMain.handle('db:location:element:delete', createHandler( - function(userId: string, data: DeleteLocationElementData, lang: 'fr' | 'en'): boolean { - return Location.deleteLocationElement(userId, data.bookId, data.elementId, data.deletedAt, lang); - } - ) -); - -// DELETE /location/sub-element/delete - Delete location sub-element -interface DeleteLocationSubElementData { - subElementId: string; - bookId: string; - deletedAt: number; -} -ipcMain.handle('db:location:subelement:delete', createHandler( - function(userId: string, data: DeleteLocationSubElementData, lang: 'fr' | 'en'): boolean { - return Location.deleteLocationSubElement(userId, data.bookId, data.subElementId, data.deletedAt, lang); - } - ) -); diff --git a/electron/ipc/offline.ipc.ts b/electron/ipc/offline.ipc.ts deleted file mode 100644 index 50cc11b..0000000 --- a/electron/ipc/offline.ipc.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { ipcMain } from 'electron'; -import { createHandler } from '../database/LocalSystem.js'; -import * as bcrypt from 'bcrypt'; -import SecureStorage, { getSecureStorage } from '../storage/SecureStorage.js'; -import { getDatabaseService } from '../database/database.service.js'; - -interface SetPinData { - pin: string; -} - -interface VerifyPinData { - pin: string; -} - -interface OfflineModeData { - enabled: boolean; - syncInterval?: number; // days -} - -ipcMain.handle('offline:pin:set', async (_event, data: SetPinData) => { - try { - const storage: SecureStorage = getSecureStorage(); - const userId: string | null = storage.get('userId'); - - if (!userId) { - return { success: false, error: 'No user logged in' }; - } - - const hashedPin: string = await bcrypt.hash(data.pin, 10); - - // Store hashed PIN - storage.set(`pin-${userId}`, hashedPin); - storage.save(); - return { success: true }; - } catch (error) { - console.error('[Offline] Error setting PIN:', error); - return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; - } -}); - -// Verify PIN for offline access -ipcMain.handle('offline:pin:verify', async (_event, data: VerifyPinData) => { - try { - const storage = getSecureStorage(); - - // Try to get last known userId - const lastUserId = storage.get('lastUserId'); - if (!lastUserId) { - return { success: false, error: 'No offline account found' }; - } - - const hashedPin = storage.get(`pin-${lastUserId}`); - if (!hashedPin) { - return { success: false, error: 'No PIN configured' }; - } - - // Verify PIN - const isValid = await bcrypt.compare(data.pin, hashedPin); - - if (isValid) { - // Set userId for session - storage.set('userId', lastUserId); - - // Initialize database for offline use - const encryptionKey = storage.get(`encryptionKey-${lastUserId}`); - if (encryptionKey) { - const db = getDatabaseService(); - db.initialize(lastUserId, encryptionKey); - } else { - console.error('[Offline] No encryption key found for user'); - return { success: false, error: 'No encryption key found' }; - } - - return { - success: true, - userId: lastUserId - }; - } - - return { success: false, error: 'Invalid PIN' }; - } catch (error) { - console.error('[Offline] Error verifying PIN:', error); - return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; - } -}); - -// Set offline mode preference -ipcMain.handle('offline:mode:set', (_event, data: OfflineModeData) => { - try { - const storage = getSecureStorage(); - storage.set('offlineMode', data.enabled); - - if (data.syncInterval) { - storage.set('syncInterval', data.syncInterval); - } - - storage.save(); - - return { success: true }; - } catch (error) { - console.error('[Offline] Error setting mode:', error); - return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; - } -}); - -// Get offline mode status -ipcMain.handle('offline:mode:get', () => { - try { - const storage = getSecureStorage(); - const offlineMode = storage.get('offlineMode', false); - const syncInterval = storage.get('syncInterval', 30); - const lastUserId = storage.get('lastUserId'); - const hasPin = lastUserId ? !!storage.get(`pin-${lastUserId}`) : false; - - return { - enabled: offlineMode, - syncInterval, - hasPin, - lastUserId - }; - } catch (error) { - console.error('[Offline] Error getting mode:', error); - return { - enabled: false, - syncInterval: 30, - hasPin: false - }; - } -}); - -// Check if should sync -ipcMain.handle('offline:sync:check', () => { - try { - const storage = getSecureStorage(); - const lastSync = storage.get('lastSync'); - const syncInterval = storage.get('syncInterval', 30) || 30; - - if (!lastSync) { - return { shouldSync: true }; - } - - const daysSinceSync = Math.floor( - (Date.now() - new Date(lastSync).getTime()) / (1000 * 60 * 60 * 24) - ); - - return { - shouldSync: daysSinceSync >= syncInterval, - daysSinceSync, - syncInterval - }; - } catch (error) { - console.error('[Offline] Error checking sync:', error); - return { shouldSync: false }; - } -}); \ No newline at end of file diff --git a/electron/ipc/series-character.ipc.ts b/electron/ipc/series-character.ipc.ts deleted file mode 100644 index a6b5c0f..0000000 --- a/electron/ipc/series-character.ipc.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { ipcMain } from 'electron'; -import { createHandler } from '../database/LocalSystem.js'; -import SeriesCharacter, { SeriesCharacterPropsPost, SeriesCharacterListProps, CharacterAttributesResponse } from '../database/models/SeriesCharacter.js'; - -interface GetCharacterListData { - seriesId: string; -} - -interface GetCharacterAttributesData { - characterId: string; -} - -interface AddCharacterData { - seriesId: string; - character: SeriesCharacterPropsPost; -} - -interface UpdateCharacterData { - character: SeriesCharacterPropsPost; -} - -interface DeleteCharacterData { - characterId: string; - deletedAt: number; -} - -interface AddAttributeData { - characterId: string; - type: string; - name: string; -} - -interface DeleteAttributeData { - attributeId: string; - deletedAt: number; -} - -// GET /series/character/list - Get character list -ipcMain.handle('db:series:character:list', createHandler( - function(userId: string, data: GetCharacterListData, lang: 'fr' | 'en'): SeriesCharacterListProps[] { - return SeriesCharacter.getCharacterList(userId, data.seriesId, lang); - } -)); - -// GET /series/character/attribute - Get character attributes -ipcMain.handle('db:series:character:attributes', createHandler( - function(userId: string, data: GetCharacterAttributesData, lang: 'fr' | 'en'): CharacterAttributesResponse { - return SeriesCharacter.getCharacterAttributes(userId, data.characterId, lang); - } -)); - -// POST /series/character/add - Add new character -ipcMain.handle('db:series:character:add', createHandler( - function(userId: string, data: AddCharacterData, lang: 'fr' | 'en'): string { - return SeriesCharacter.addNewCharacter(userId, data.character, data.seriesId, lang); - } -)); - -// PATCH /series/character/update - Update character -ipcMain.handle('db:series:character:update', createHandler( - function(userId: string, data: UpdateCharacterData, lang: 'fr' | 'en'): boolean { - return SeriesCharacter.updateCharacter(userId, data.character, lang); - } -)); - -// DELETE /series/character/delete - Delete character -ipcMain.handle('db:series:character:delete', createHandler( - function(userId: string, data: DeleteCharacterData, lang: 'fr' | 'en'): boolean { - return SeriesCharacter.deleteCharacter(userId, data.characterId, data.deletedAt, lang); - } -)); - -// POST /series/character/attribute/add - Add attribute -ipcMain.handle('db:series:character:attribute:add', createHandler( - function(userId: string, data: AddAttributeData, lang: 'fr' | 'en'): string { - return SeriesCharacter.addNewAttribute(data.characterId, userId, data.type, data.name, lang); - } -)); - -// DELETE /series/character/attribute/delete - Delete attribute -ipcMain.handle('db:series:character:attribute:delete', createHandler( - function(userId: string, data: DeleteAttributeData, lang: 'fr' | 'en'): boolean { - return SeriesCharacter.deleteAttribute(userId, data.attributeId, data.deletedAt, lang); - } -)); diff --git a/electron/ipc/series-location.ipc.ts b/electron/ipc/series-location.ipc.ts deleted file mode 100644 index c18480b..0000000 --- a/electron/ipc/series-location.ipc.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { ipcMain } from 'electron'; -import { createHandler } from '../database/LocalSystem.js'; -import SeriesLocation, { SeriesLocationListProps } from '../database/models/SeriesLocation.js'; - -interface GetLocationListData { - seriesId: string; -} - -interface AddLocationSectionData { - seriesId: string; - name: string; -} - -interface AddElementData { - locationId: string; - name: string; - description?: string; -} - -interface AddSubElementData { - elementId: string; - name: string; - description?: string; -} - -interface DeleteLocationData { - locationId: string; - deletedAt: number; -} - -interface DeleteElementData { - elementId: string; - deletedAt: number; -} - -interface DeleteSubElementData { - subElementId: string; - deletedAt: number; -} - -// GET /series/location/list - Get location list -ipcMain.handle('db:series:location:list', createHandler( - function(userId: string, data: GetLocationListData, lang: 'fr' | 'en'): SeriesLocationListProps[] { - return SeriesLocation.getLocationList(userId, data.seriesId, lang); - } -)); - -// POST /series/location/section/add - Add location section -ipcMain.handle('db:series:location:section:add', createHandler( - function(userId: string, data: AddLocationSectionData, lang: 'fr' | 'en'): string { - return SeriesLocation.addLocationSection(userId, data.seriesId, data.name, lang); - } -)); - -// POST /series/location/element/add - Add element -ipcMain.handle('db:series:location:element:add', createHandler( - function(userId: string, data: AddElementData, lang: 'fr' | 'en'): string { - return SeriesLocation.addElement(userId, data.locationId, data.name, lang, data.description); - } -)); - -// POST /series/location/sub-element/add - Add sub-element -ipcMain.handle('db:series:location:subelement:add', createHandler( - function(userId: string, data: AddSubElementData, lang: 'fr' | 'en'): string { - return SeriesLocation.addSubElement(userId, data.elementId, data.name, lang, data.description); - } -)); - -// DELETE /series/location/delete - Delete location -ipcMain.handle('db:series:location:delete', createHandler( - function(userId: string, data: DeleteLocationData, lang: 'fr' | 'en'): boolean { - return SeriesLocation.deleteLocation(userId, data.locationId, data.deletedAt, lang); - } -)); - -// DELETE /series/location/element/delete - Delete element -ipcMain.handle('db:series:location:element:delete', createHandler( - function(userId: string, data: DeleteElementData, lang: 'fr' | 'en'): boolean { - return SeriesLocation.deleteElement(userId, data.elementId, data.deletedAt, lang); - } -)); - -// DELETE /series/location/sub-element/delete - Delete sub-element -ipcMain.handle('db:series:location:subelement:delete', createHandler( - function(userId: string, data: DeleteSubElementData, lang: 'fr' | 'en'): boolean { - return SeriesLocation.deleteSubElement(userId, data.subElementId, data.deletedAt, lang); - } -)); diff --git a/electron/ipc/series-spell.ipc.ts b/electron/ipc/series-spell.ipc.ts deleted file mode 100644 index aeb6e10..0000000 --- a/electron/ipc/series-spell.ipc.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { ipcMain } from 'electron'; -import { createHandler } from '../database/LocalSystem.js'; -import SeriesSpell, { SeriesSpellListResponse, SeriesSpellDetailProps } from '../database/models/SeriesSpell.js'; - -interface GetSpellListData { - seriesId: string; -} - -interface GetSpellDetailData { - spellId: string; -} - -interface AddSpellData { - seriesId: string; - name: string; - description?: string | null; - appearance?: string | null; - tags?: string[]; - powerLevel?: string | null; - components?: string | null; - limitations?: string | null; - notes?: string | null; -} - -interface UpdateSpellData { - id: string; - name: string; - description?: string | null; - appearance?: string | null; - tags?: string[]; - powerLevel?: string | null; - components?: string | null; - limitations?: string | null; - notes?: string | null; -} - -interface DeleteSpellData { - spellId: string; - deletedAt: number; -} - -interface AddTagData { - seriesId: string; - name: string; - color?: string | null; -} - -interface UpdateTagData { - tagId: string; - name: string; - color?: string | null; -} - -interface DeleteTagData { - tagId: string; - deletedAt: number; -} - -// GET /series/spell/list - Get spell list -ipcMain.handle('db:series:spell:list', createHandler( - function(userId: string, data: GetSpellListData, lang: 'fr' | 'en'): SeriesSpellListResponse { - return SeriesSpell.getSpellList(userId, data.seriesId, lang); - } -)); - -// GET /series/spell/detail - Get spell detail -ipcMain.handle('db:series:spell:detail', createHandler( - function(userId: string, data: GetSpellDetailData, lang: 'fr' | 'en'): SeriesSpellDetailProps { - return SeriesSpell.getSpellDetail(userId, data.spellId, lang); - } -)); - -// POST /series/spell/add - Add spell -ipcMain.handle('db:series:spell:add', createHandler( - function(userId: string, data: AddSpellData, lang: 'fr' | 'en'): string { - return SeriesSpell.addSpell(userId, data.seriesId, data.name, lang, data.description, data.appearance, data.tags, data.powerLevel, data.components, data.limitations, data.notes); - } -)); - -// PUT /series/spell/update - Update spell -ipcMain.handle('db:series:spell:update', createHandler( - function(userId: string, data: UpdateSpellData, lang: 'fr' | 'en'): boolean { - return SeriesSpell.updateSpell(userId, data.id, data.name, lang, data.description, data.appearance, data.tags, data.powerLevel, data.components, data.limitations, data.notes); - } -)); - -// DELETE /series/spell/delete - Delete spell -ipcMain.handle('db:series:spell:delete', createHandler( - function(userId: string, data: DeleteSpellData, lang: 'fr' | 'en'): boolean { - return SeriesSpell.deleteSpell(userId, data.spellId, data.deletedAt, lang); - } -)); - -// POST /series/spell/tag/add - Add tag -ipcMain.handle('db:series:spell:tag:add', createHandler( - function(userId: string, data: AddTagData, lang: 'fr' | 'en'): string { - return SeriesSpell.addTag(userId, data.seriesId, data.name, lang, data.color); - } -)); - -// PUT /series/spell/tag/update - Update tag -ipcMain.handle('db:series:spell:tag:update', createHandler( - function(userId: string, data: UpdateTagData, lang: 'fr' | 'en'): boolean { - return SeriesSpell.updateTag(userId, data.tagId, data.name, lang, data.color); - } -)); - -// DELETE /series/spell/tag/delete - Delete tag -ipcMain.handle('db:series:spell:tag:delete', createHandler( - function(userId: string, data: DeleteTagData, lang: 'fr' | 'en'): boolean { - return SeriesSpell.deleteTag(userId, data.tagId, data.deletedAt, lang); - } -)); diff --git a/electron/ipc/series-sync.ipc.ts b/electron/ipc/series-sync.ipc.ts deleted file mode 100644 index 172e3db..0000000 --- a/electron/ipc/series-sync.ipc.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ipcMain } from 'electron'; -import { createHandler } from '../database/LocalSystem.js'; -import SeriesSync, { SeriesSyncUploadPayload, SeriesSyncResult, CompleteSeries, SyncedSeries } from '../database/models/SeriesSync.js'; -import { SyncElementType } from '../database/repositories/series-sync.repo.js'; - -interface UploadToSeriesData { - type: SyncElementType; - bookElementId: string; - field: string; - value: string; -} - -ipcMain.handle('db:series:sync:upload', createHandler( - function(userId: string, data: UploadToSeriesData, lang: 'fr' | 'en'): SeriesSyncResult { - const payload: SeriesSyncUploadPayload = { - type: data.type, - bookElementId: data.bookElementId, - field: data.field, - value: data.value || '' - }; - return SeriesSync.uploadFieldToSeries(userId, payload, lang); - } -)); - -ipcMain.handle('db:series:synced', createHandler( - function(userId: string, _data: void, lang: 'fr' | 'en'): SyncedSeries[] { - return SeriesSync.getSyncedSeries(userId, lang); - } -)); - -ipcMain.handle('db:series:uploadToServer', createHandler( - async function(userId: string, seriesId: string, lang: 'fr' | 'en'): Promise { - return SeriesSync.getCompleteSeriesForUpload(userId, seriesId, lang); - } -)); - -ipcMain.handle('db:series:syncSave', createHandler( - async function(userId: string, completeSeries: CompleteSeries, lang: 'fr' | 'en'): Promise { - return SeriesSync.saveCompleteSeries(userId, completeSeries, lang); - } -)); - -ipcMain.handle('db:series:sync:toClient', createHandler( - async function(userId: string, completeSeries: CompleteSeries, lang: 'fr' | 'en'): Promise { - return SeriesSync.syncSeriesFromServerToClient(userId, completeSeries, lang); - } -)); - -ipcMain.handle('db:series:sync:toServer', createHandler( - async function(userId: string, syncCompare: object, lang: 'fr' | 'en'): Promise { - const seriesId = (syncCompare as { id: string }).id; - return SeriesSync.getCompleteSeriesForUpload(userId, seriesId, lang); - } -)); diff --git a/electron/ipc/series-world.ipc.ts b/electron/ipc/series-world.ipc.ts deleted file mode 100644 index 580fd03..0000000 --- a/electron/ipc/series-world.ipc.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { ipcMain } from 'electron'; -import { createHandler } from '../database/LocalSystem.js'; -import SeriesWorld, { SeriesWorldListProps, SeriesWorldUpdateProps } from '../database/models/SeriesWorld.js'; - -interface GetWorldListData { - seriesId: string; -} - -interface AddWorldData { - seriesId: string; - name: string; -} - -interface UpdateWorldData { - worldId: string; - name: string; - history?: string; - politics?: string; - economy?: string; - religion?: string; - languages?: string; -} - -interface AddElementData { - worldId: string; - elementType: number; - name: string; - description?: string; -} - -interface DeleteElementData { - elementId: string; - deletedAt: number; -} - -// GET /series/world/list - Get world list -ipcMain.handle('db:series:world:list', createHandler( - function(userId: string, data: GetWorldListData, lang: 'fr' | 'en'): SeriesWorldListProps[] { - return SeriesWorld.getWorldList(userId, data.seriesId, lang); - } -)); - -// POST /series/world/add - Add world -ipcMain.handle('db:series:world:add', createHandler( - function(userId: string, data: AddWorldData, lang: 'fr' | 'en'): string { - return SeriesWorld.addWorld(userId, data.seriesId, data.name, lang); - } -)); - -// PATCH /series/world/update - Update world -ipcMain.handle('db:series:world:update', createHandler( - function(userId: string, data: UpdateWorldData, lang: 'fr' | 'en'): boolean { - const worldData: SeriesWorldUpdateProps = { - name: data.name, - history: data.history, - politics: data.politics, - economy: data.economy, - religion: data.religion, - languages: data.languages - }; - return SeriesWorld.updateWorld(userId, data.worldId, worldData, lang); - } -)); - -// POST /series/world/element/add - Add element -ipcMain.handle('db:series:world:element:add', createHandler( - function(userId: string, data: AddElementData, lang: 'fr' | 'en'): string { - return SeriesWorld.addElement(userId, data.worldId, data.elementType, data.name, lang, data.description); - } -)); - -// DELETE /series/world/element/delete - Delete element -ipcMain.handle('db:series:world:element:delete', createHandler( - function(userId: string, data: DeleteElementData, lang: 'fr' | 'en'): boolean { - return SeriesWorld.deleteElement(userId, data.elementId, data.deletedAt, lang); - } -)); diff --git a/electron/ipc/series.ipc.ts b/electron/ipc/series.ipc.ts deleted file mode 100644 index d03affe..0000000 --- a/electron/ipc/series.ipc.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { ipcMain } from 'electron'; -import { createHandler } from '../database/LocalSystem.js'; -import Series, { BooksOrderPost, SeriesDetailProps, SeriesListItemProps, SeriesBookProps } from '../database/models/Series.js'; - -interface CreateSeriesData { - name: string; - description?: string; - bookIds?: string[]; -} - -interface UpdateSeriesData { - seriesId: string; - name: string; - description?: string; -} - -interface DeleteSeriesData { - seriesId: string; - deletedAt: number; -} - -interface GetSeriesDetailData { - seriesId: string; -} - -interface AddBookToSeriesData { - seriesId: string; - bookId: string; - order?: number; -} - -interface RemoveBookFromSeriesData { - seriesId: string; - bookId: string; - deletedAt: number; -} - -interface UpdateBooksOrderData { - seriesId: string; - booksOrder: BooksOrderPost[]; -} - -interface GetSeriesForBookData { - bookId: string; -} - -interface GetSeriesBooksData { - seriesId: string; -} - -// GET /series/list - Get all series -ipcMain.handle('db:series:list', createHandler( - async function(userId: string, _body: void, lang: 'fr' | 'en'): Promise { - return await Series.getSeriesList(userId, lang); - } -)); - -// GET /series/detail - Get series detail -ipcMain.handle('db:series:detail', createHandler( - async function(userId: string, data: GetSeriesDetailData, lang: 'fr' | 'en'): Promise { - return await Series.getSeriesDetail(userId, data.seriesId, lang); - } -)); - -// POST /series/add - Create new series -ipcMain.handle('db:series:create', createHandler( - async function(userId: string, data: CreateSeriesData, lang: 'fr' | 'en'): Promise { - return await Series.createSeries(userId, data.name, data.description || '', lang, data.bookIds); - } -)); - -// PUT /series/update - Update series -ipcMain.handle('db:series:update', createHandler( - async function(userId: string, data: UpdateSeriesData, lang: 'fr' | 'en'): Promise { - return await Series.updateSeries(userId, data.seriesId, data.name, data.description || '', lang); - } -)); - -// DELETE /series/delete - Delete series -ipcMain.handle('db:series:delete', createHandler( - async function(userId: string, data: DeleteSeriesData, lang: 'fr' | 'en'): Promise { - return await Series.deleteSeries(userId, data.seriesId, data.deletedAt, lang); - } -)); - -// GET /series/book/list - Get books in series -ipcMain.handle('db:series:books', createHandler( - async function(userId: string, data: GetSeriesBooksData, lang: 'fr' | 'en'): Promise { - return await Series.getSeriesBooks(userId, data.seriesId, lang); - } -)); - -// POST /series/book/add - Add book to series -ipcMain.handle('db:series:book:add', createHandler( - async function(userId: string, data: AddBookToSeriesData, lang: 'fr' | 'en'): Promise { - return await Series.addBookToSeries(userId, data.seriesId, data.bookId, data.order ?? 1, lang); - } -)); - -// DELETE /series/book/remove - Remove book from series -ipcMain.handle('db:series:book:remove', createHandler( - async function(userId: string, data: RemoveBookFromSeriesData, lang: 'fr' | 'en'): Promise { - return await Series.removeBookFromSeries(userId, data.seriesId, data.bookId, data.deletedAt, lang); - } -)); - -// PUT /series/book/reorder - Reorder books in series -ipcMain.handle('db:series:book:reorder', createHandler( - async function(userId: string, data: UpdateBooksOrderData, lang: 'fr' | 'en'): Promise { - return await Series.updateBooksOrder(userId, data.seriesId, data.booksOrder, lang); - } -)); - -// GET /series/for-book - Get series ID for a book -ipcMain.handle('db:series:forBook', createHandler( - function(_userId: string, data: GetSeriesForBookData, _lang: 'fr' | 'en'): string | null { - return Series.getSeriesIdForBook(data.bookId); - } -)); diff --git a/electron/ipc/spell.ipc.ts b/electron/ipc/spell.ipc.ts deleted file mode 100644 index 8716359..0000000 --- a/electron/ipc/spell.ipc.ts +++ /dev/null @@ -1,207 +0,0 @@ -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; - seriesSpellId?: 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; - bookId: string; - deletedAt: number; -} - -interface CreateTagData { - bookId: string; - name: string; - color?: string | null; -} - -interface UpdateTagData { - tagId: string; - name: string; - color?: string | null; -} - -interface DeleteTagData { - tagId: string; - bookId: string; - deletedAt: number; -} - -// ==================== SPELL HANDLERS ==================== - -// GET /spell/list -ipcMain.handle( - 'db:spell:list', - createHandler( - 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( - 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( - 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( - 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, - spell.seriesSpellId || null, - ); - return result.id; - }, - ), -); - -// PUT /spell/update -ipcMain.handle( - 'db:spell:update', - createHandler( - 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, - spell.seriesSpellId || null, - ); - }, - ), -); - -// DELETE /spell/delete -ipcMain.handle( - 'db:spell:delete', - createHandler( - function (userId: string, data: DeleteSpellData, lang: 'fr' | 'en'): boolean { - return Spell.deleteSpell(userId, data.bookId, data.spellId, data.deletedAt, lang); - }, - ), -); - -// ==================== SPELL TAG HANDLERS ==================== - -// POST /spell/tag/add -ipcMain.handle( - 'db:spell:tag:create', - createHandler( - 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( - 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( - function (userId: string, data: DeleteTagData, lang: 'fr' | 'en'): boolean { - return Spell.deleteSpellTag(userId, data.bookId, data.tagId, data.deletedAt, lang); - }, - ), -); diff --git a/electron/ipc/tombstone.ipc.ts b/electron/ipc/tombstone.ipc.ts deleted file mode 100644 index 87e41f8..0000000 --- a/electron/ipc/tombstone.ipc.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { ipcMain } from 'electron'; -import { createHandler } from '../database/LocalSystem.js'; -import RemovedItemsRepository, { RemovedItemRecord } from '../database/repositories/removed-items.repository.js'; -import Book from '../database/models/Book.js'; -import Chapter from '../database/models/Chapter.js'; -import Character from '../database/models/Character.js'; -import Location from '../database/models/Location.js'; -import World from '../database/models/World.js'; -import Incident from '../database/models/Incident.js'; -import PlotPoint from '../database/models/PlotPoint.js'; -import Issue from '../database/models/Issue.js'; -import Spell from '../database/models/Spell.js'; -import Series from '../database/models/Series.js'; -import SeriesCharacter from '../database/models/SeriesCharacter.js'; -import SeriesLocation from '../database/models/SeriesLocation.js'; -import SeriesWorld from '../database/models/SeriesWorld.js'; -import SeriesSpell from '../database/models/SeriesSpell.js'; - -/** - * Get tombstones since a specific timestamp. - */ -ipcMain.handle('db:tombstones:since', createHandler( - function(userId: string, since: number, lang: 'fr' | 'en'): RemovedItemRecord[] { - return RemovedItemsRepository.getDeletionsSince(userId, since, lang); - }) -); - -/** - * Apply server tombstones for book entities locally. - */ -ipcMain.handle('db:tombstones:apply:books', createHandler( - function(userId: string, tombstones: RemovedItemRecord[], lang: 'fr' | 'en'): void { - for (const tombstone of tombstones) { - switch (tombstone.table_name) { - case 'erit_books': - Book.removeBook(userId, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'book_chapters': - Chapter.removeChapter(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'book_chapter_infos': - Chapter.removeChapterInformation(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'book_characters': - Character.deleteCharacter(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'book_characters_attributes': - Character.deleteAttribute(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'book_location': - Location.deleteLocationSection(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'location_element': - Location.deleteLocationElement(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'location_sub_element': - Location.deleteLocationSubElement(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'book_world_elements': - World.removeElementFromWorld(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'book_incidents': - Incident.removeIncident(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'book_plot_points': - PlotPoint.removePlotPoint(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'book_issues': - Issue.removeIssue(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'book_spells': - Spell.deleteSpell(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'book_spell_tags': - Spell.deleteSpellTag(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - } - } - }) -); - -/** - * Apply server tombstones for series entities locally. - */ -ipcMain.handle('db:tombstones:apply:series', createHandler( - function(userId: string, tombstones: RemovedItemRecord[], lang: 'fr' | 'en'): void { - for (const tombstone of tombstones) { - switch (tombstone.table_name) { - case 'erit_series': - Series.deleteSeries(userId, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'series_books': - Series.removeBookFromSeries(userId, tombstone.book_id!, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'series_characters': - SeriesCharacter.deleteCharacter(userId, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'series_characters_attributes': - SeriesCharacter.deleteAttribute(userId, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'series_locations': - SeriesLocation.deleteLocation(userId, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'series_location_elements': - SeriesLocation.deleteElement(userId, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'series_location_sub_elements': - SeriesLocation.deleteSubElement(userId, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'series_world_elements': - SeriesWorld.deleteElement(userId, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'series_spells': - SeriesSpell.deleteSpell(userId, tombstone.entity_id, tombstone.deleted_at, lang); - break; - case 'series_spell_tags': - SeriesSpell.deleteTag(userId, tombstone.entity_id, tombstone.deleted_at, lang); - break; - } - } - }) -); diff --git a/electron/ipc/user.ipc.ts b/electron/ipc/user.ipc.ts deleted file mode 100644 index 412e490..0000000 --- a/electron/ipc/user.ipc.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ipcMain } from 'electron'; -import { createHandler } from '../database/LocalSystem.js'; -import User, {UserInfoResponse} from '../database/models/User.js'; - -interface UpdateUserData { - firstName: string; - lastName: string; - username: string; - email: string; - authorName?: string; -} - -// GET /user/info - Get user info from local DB -ipcMain.handle('db:user:info', createHandler( - async function(userId: string, _body: void, _lang: 'fr' | 'en'):Promise { - return await User.returnUserInfos(userId); - } -)); - -// PUT /user/update - Update user info in local DB -ipcMain.handle('db:user:update', createHandler( - async function(userId: string, data: UpdateUserData, lang: 'fr' | 'en'):Promise { - const userKey = ''; - return await User.updateUserInfos(userKey, userId, data.firstName, data.lastName, data.username, data.email, data.authorName, lang); - } -)); diff --git a/electron/main.ts b/electron/main.ts deleted file mode 100644 index ec70549..0000000 --- a/electron/main.ts +++ /dev/null @@ -1,826 +0,0 @@ -import {app, BrowserWindow, dialog, ipcMain, IpcMainInvokeEvent, Menu, nativeImage, protocol, safeStorage, shell} from 'electron'; -import * as path from 'path'; -import {fileURLToPath} from 'url'; -import * as fs from 'fs'; -import {DatabaseService, getDatabaseService} from './database/database.service.js'; -import SecureStorage, {getSecureStorage} from './storage/SecureStorage.js'; -import {getUserEncryptionKey, hasUserEncryptionKey, setUserEncryptionKey} from './database/keyManager.js'; -import {generateUserEncryptionKey} from './database/encryption.js'; -import {initAutoUpdater} from './autoUpdater.js'; - -// Import IPC handlers -import './ipc/book.ipc.js'; -import './ipc/user.ipc.js'; -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'; -import './ipc/series.ipc.js'; -import './ipc/series-sync.ipc.js'; -import './ipc/series-character.ipc.js'; -import './ipc/series-location.ipc.js'; -import './ipc/series-world.ipc.js'; -import './ipc/series-spell.ipc.js'; -import './ipc/tombstone.ipc.js'; - -// Fix pour __dirname en ES modules -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const isDev = !app.isPackaged; - -// Enregistrer le protocole scribedesktop:// comme standard (avant app.whenReady) -if (!isDev) { - protocol.registerSchemesAsPrivileged([ - { - scheme: 'scribedesktop', - privileges: { - standard: true, - secure: true, - supportFetchAPI: true, - corsEnabled: true - } - } - ]); -} - -// Définir le nom de l'application -app.setName('ERitors Scribe'); - -// En dev et prod, __dirname pointe vers dist/electron/ -const preloadPath = path.join(__dirname, 'preload.js'); - -// Icône de l'application -const iconPath = isDev - ? path.join(__dirname, '../build/icon.png') - : process.platform === 'darwin' - ? path.join(process.resourcesPath, 'icon.icns') // macOS utilise .icns - : path.join(process.resourcesPath, 'app.asar/build/icon.png'); // Windows/Linux utilisent .png - -let mainWindow: BrowserWindow | null = null; -let loginWindow: BrowserWindow | null = null; - -function createLoginWindow(): void { - loginWindow = new BrowserWindow({ - width: 500, - height: 900, - resizable: false, - ...(process.platform !== 'darwin' && { icon: iconPath }), - autoHideMenuBar: true, - webPreferences: { - preload: preloadPath, - contextIsolation: true, - nodeIntegration: false, - sandbox: true, - webSecurity: true, - allowRunningInsecureContent: false, - experimentalFeatures: false, - enableBlinkFeatures: '', - disableBlinkFeatures: '', - webviewTag: false, - navigateOnDragDrop: false, - }, - frame: true, - show: false, - }); - - if (isDev) { - const devPort = process.env.PORT || '4000'; - loginWindow.loadURL(`http://localhost:${devPort}/login/login`); - loginWindow.webContents.openDevTools(); - } else { - loginWindow.loadURL('scribedesktop://./login/login/index.html'); - } - - loginWindow.once('ready-to-show', () => { - loginWindow?.show(); - if (loginWindow) { - initAutoUpdater(loginWindow); - } - }); - - loginWindow.on('closed', () => { - loginWindow = null; - }); - - // Security: Block navigation to external domains - loginWindow.webContents.on('will-navigate', (event, navigationUrl) => { - const parsedUrl = new URL(navigationUrl); - if (isDev) { - if (!parsedUrl.origin.startsWith('http://localhost')) { - event.preventDefault(); - } - } else { - if (parsedUrl.protocol !== 'scribedesktop:') { - event.preventDefault(); - } - } - }); - - // Security: Block new window creation - loginWindow.webContents.setWindowOpenHandler(() => { - return { action: 'deny' }; - }); -} - -function createMainWindow(): void { - mainWindow = new BrowserWindow({ - width: 1200, - height: 800, - ...(process.platform !== 'darwin' && { icon: iconPath }), - autoHideMenuBar: true, - webPreferences: { - preload: preloadPath, - contextIsolation: true, - nodeIntegration: false, - sandbox: true, - webSecurity: true, - allowRunningInsecureContent: false, - experimentalFeatures: false, - enableBlinkFeatures: '', - disableBlinkFeatures: '', - webviewTag: false, - navigateOnDragDrop: false, - }, - show: false, - }); - - if (isDev) { - const devPort = process.env.PORT || '4000'; - mainWindow.loadURL(`http://localhost:${devPort}`); - mainWindow.webContents.openDevTools(); - } else { - mainWindow.loadURL('scribedesktop://./index.html'); - } - - mainWindow.once('ready-to-show', () => { - mainWindow?.show(); - if (mainWindow) { - initAutoUpdater(mainWindow); - } - }); - - mainWindow.on('closed', () => { - mainWindow = null; - }); - - // Security: Block navigation to external domains - mainWindow.webContents.on('will-navigate', (event, navigationUrl) => { - const parsedUrl = new URL(navigationUrl); - if (isDev) { - if (!parsedUrl.origin.startsWith('http://localhost')) { - event.preventDefault(); - } - } else { - if (parsedUrl.protocol !== 'scribedesktop:') { - event.preventDefault(); - } - } - }); - - // Security: Block new window creation - mainWindow.webContents.setWindowOpenHandler(() => { - return { action: 'deny' }; - }); -} - -// IPC Handler pour ouvrir des liens externes (navigateur/app native) -ipcMain.handle('open-external', async (_event, url: string) => { - // Security: Validate URL before opening - try { - const parsedUrl = new URL(url); - const allowedProtocols = ['http:', 'https:', 'mailto:']; - - if (!allowedProtocols.includes(parsedUrl.protocol)) { - console.error('[Security] Blocked external URL with invalid protocol:', parsedUrl.protocol); - return; - } - - await shell.openExternal(url); - } catch (error) { - console.error('[Security] Invalid URL rejected:', url); - } -}); - -// IPC Handler pour OAuth login via BrowserWindow -let oauthWindow: BrowserWindow | null = null; - -interface OAuthResult { - success: boolean; - code?: string; - state?: string; - error?: string; -} - -interface OAuthRequest { - provider: 'google' | 'facebook' | 'apple'; - baseUrl: string; -} - -ipcMain.handle('oauth-login', async (_event, request: OAuthRequest): Promise => { - return new Promise((resolve) => { - const { provider, baseUrl } = request; - const redirectUri = `${baseUrl}login?provider=${provider}`; - const encodedRedirectUri = encodeURIComponent(redirectUri); - - // Fermer une éventuelle fenêtre OAuth existante - if (oauthWindow) { - oauthWindow.close(); - oauthWindow = null; - } - - // Configuration OAuth par provider - const oauthConfigs: Record = { - google: `https://accounts.google.com/o/oauth2/v2/auth?client_id=911482317931-pvjog1br22r6l8k1afq0ki94em2fsoen.apps.googleusercontent.com&redirect_uri=${encodedRedirectUri}&response_type=code&scope=openid%20email%20profile&access_type=offline`, - facebook: `https://www.facebook.com/v18.0/dialog/oauth?client_id=1015270470233591&redirect_uri=${encodedRedirectUri}&scope=email&response_type=code&state=abc123`, - apple: `https://appleid.apple.com/auth/authorize?client_id=eritors.apple.login&redirect_uri=${encodedRedirectUri}&response_type=code&scope=email%20name&response_mode=query&state=abc123` - }; - - const authUrl = oauthConfigs[provider]; - if (!authUrl) { - resolve({ success: false, error: 'Invalid provider' }); - return; - } - - oauthWindow = new BrowserWindow({ - width: 600, - height: 700, - show: true, - autoHideMenuBar: true, - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - } - }); - - // Intercepter les redirections pour capturer le code OAuth - const handleNavigation = (url: string): boolean => { - try { - const parsedUrl = new URL(url); - - // Vérifier si c'est notre redirect URI (compare sans le query string) - const baseRedirectUri = `${baseUrl}login`; - if (url.startsWith(baseRedirectUri)) { - const code = parsedUrl.searchParams.get('code'); - const state = parsedUrl.searchParams.get('state'); - const error = parsedUrl.searchParams.get('error'); - - if (error) { - resolve({ success: false, error }); - } else if (code) { - resolve({ success: true, code, state: state || undefined }); - } else { - resolve({ success: false, error: 'No code received' }); - } - - if (oauthWindow) { - oauthWindow.close(); - oauthWindow = null; - } - return true; // Navigation interceptée - } - } catch (e) { - // URL invalide, continuer - } - return false; // Laisser la navigation continuer - }; - - // Écouter will-redirect (redirections HTTP) - oauthWindow.webContents.on('will-redirect', (event, url) => { - if (handleNavigation(url)) { - event.preventDefault(); - } - }); - - // Écouter will-navigate (navigations normales) - oauthWindow.webContents.on('will-navigate', (event, url) => { - if (handleNavigation(url)) { - event.preventDefault(); - } - }); - - // Gérer la fermeture de la fenêtre par l'utilisateur - oauthWindow.on('closed', () => { - oauthWindow = null; - resolve({ success: false, error: 'Window closed by user' }); - }); - - // Charger l'URL OAuth - oauthWindow.loadURL(authUrl); - }); -}); - -// IPC Handlers pour la gestion du token (OS-encrypted storage) -ipcMain.handle('get-token', () => { - const storage:SecureStorage = getSecureStorage(); - return storage.get('authToken', null); -}); - -ipcMain.handle('set-token', (_event, token: string) => { - const storage = getSecureStorage(); - storage.set('authToken', token); - return true; -}); - -ipcMain.handle('remove-token', () => { - const storage = getSecureStorage(); - storage.delete('authToken'); - return true; -}); - -// IPC Handlers pour la gestion de la langue -ipcMain.handle('get-lang', () => { - const storage = getSecureStorage(); - return storage.get('userLang', 'fr'); -}); - -ipcMain.handle('set-lang', (_event, lang: 'fr' | 'en') => { - const storage = getSecureStorage(); - storage.set('userLang', lang); - return true; -}); - -// IPC Handler pour initialiser l'utilisateur après récupération depuis le serveur -ipcMain.handle('init-user', async (_event:IpcMainInvokeEvent, userId: string) => { - const storage:SecureStorage = getSecureStorage(); - storage.set('userId', userId); - storage.set('lastUserId', userId); - - try { - let encryptionKey: string | null; - - if (!hasUserEncryptionKey(userId)) { - encryptionKey = generateUserEncryptionKey(userId); - - if (!encryptionKey) { - throw new Error('Failed to generate encryption key'); - } - - setUserEncryptionKey(userId, encryptionKey); - - const savedKey:string = getUserEncryptionKey(userId); - - if (!savedKey) { - throw new Error('Failed to save encryption key'); - } - } else { - encryptionKey = getUserEncryptionKey(userId); - - if (!encryptionKey) { - console.error('[InitUser] CRITICAL: Existing key is undefined, regenerating'); - const { generateUserEncryptionKey } = await import('./database/encryption.js'); - encryptionKey = generateUserEncryptionKey(userId); - setUserEncryptionKey(userId, encryptionKey); - } - } - - if (safeStorage.isEncryptionAvailable()) { - storage.save(); - } else { - return { - success: false, - error: 'Encryption is not available on this system' - }; - } - - return { success: true, keyCreated: !hasUserEncryptionKey(userId) }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } -}); - -ipcMain.on('login-success', async (_event, token: string) => { - const storage = getSecureStorage(); - storage.set('authToken', token); - - if (loginWindow) { - loginWindow.close(); - } - - createMainWindow(); - - setTimeout(async ():Promise => { - try { - if (safeStorage.isEncryptionAvailable()) { - storage.save(); - } else { - setTimeout(() => { - if (safeStorage.isEncryptionAvailable()) { - storage.save(); - } else { - console.error('[Login] CRITICAL: Cannot encrypt credentials'); - } - }, 1000); - } - } catch (error) { - console.error('[Login] Error saving auth data:', error); - } - }, 500); -}); - -ipcMain.on('logout', ():void => { - try { - const storage:SecureStorage = getSecureStorage(); - - storage.delete('authToken'); - storage.delete('userId'); - storage.delete('userLang'); - - storage.save(); - } catch (error) { - console.error('[Logout] Error clearing storage:', error); - } - - try { - const db:DatabaseService = getDatabaseService(); - db.close(); - } catch (error) { - console.error('[Logout] Error closing database:', error); - } - - if (mainWindow) { - mainWindow.close(); - mainWindow = null; - } - - createLoginWindow(); -}); - -// ========== MIGRATION EXPORT (Electron → Tauri) ========== - -ipcMain.handle('export-migration', async ():Promise<{success: boolean; path?: string; error?: string}> => { - const storage:SecureStorage = getSecureStorage(); - const userId:string | null = storage.get('userId', null); - const lastUserId:string | null = storage.get('lastUserId', null); - - const targetUserId:string | null = userId || lastUserId; - if (!targetUserId) { - return { success: false, error: 'No user found in storage' }; - } - - let encryptionKey:string | null = null; - try { - encryptionKey = getUserEncryptionKey(targetUserId); - } catch { - return { success: false, error: 'Encryption key not found for this user' }; - } - - const pinHash:string | null = storage.get(`pin-${targetUserId}`, null); - - const userDataPath:string = app.getPath('userData'); - const dbPath:string = path.join(userDataPath, 'eritors-local.db'); - - if (!fs.existsSync(dbPath)) { - return { success: false, error: 'No local database found' }; - } - - const { filePath } = await dialog.showSaveDialog({ - title: 'Export migration data', - defaultPath: path.join(app.getPath('desktop'), 'eritors-migration.json'), - filters: [{ name: 'Migration', extensions: ['json'] }], - }); - - if (!filePath) { - return { success: false, error: 'Export cancelled' }; - } - - const migrationData = { - version: 1, - exported_at: Date.now(), - user_id: targetUserId, - encryption_key: encryptionKey, - pin_hash: pinHash, - db_source: dbPath, - }; - - try { - fs.writeFileSync(filePath, JSON.stringify(migrationData, null, 2), 'utf-8'); - - // Copier aussi la DB à côté du fichier de migration - const dbDestination:string = path.join(path.dirname(filePath), `eritors-local-${targetUserId}.db`); - fs.copyFileSync(dbPath, dbDestination); - - // Copier WAL et SHM si existants - const walPath:string = dbPath + '-wal'; - const shmPath:string = dbPath + '-shm'; - if (fs.existsSync(walPath)) { - fs.copyFileSync(walPath, dbDestination + '-wal'); - } - if (fs.existsSync(shmPath)) { - fs.copyFileSync(shmPath, dbDestination + '-shm'); - } - - return { success: true, path: filePath }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Export failed', - }; - } -}); - -// ========== USER SYNC (PRE-AUTHENTICATION) ========== - -interface SyncUserData { - userId: string; - firstName: string; - lastName: string; - username: string; - email: string; -} - -ipcMain.handle('db:user:sync', async (_event, data: SyncUserData): Promise => { - try { - const { default: User } = await import('./database/models/User.js'); - const { default: UserRepo } = await import('./database/repositories/user.repository.js'); - - const lang: 'fr' | 'en' = 'fr'; - try { - UserRepo.fetchUserInfos(data.userId, lang); - return true; - } catch (error) { - await User.addUser( - data.userId, - data.firstName, - data.lastName, - data.username, - data.email, - '', - lang - ); - return true; - } - } catch (error) { - throw error; - } -}); - -/** - * Generate user encryption key - */ -ipcMain.handle('generate-encryption-key', async (_event, userId: string) => { - try { - const key:string = generateUserEncryptionKey(userId); - return { success: true, key }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } -}); - -/** - * Get or generate user encryption key (OS-encrypted storage) - */ -ipcMain.handle('get-user-encryption-key', (_event, userId: string) => { - const storage:SecureStorage = getSecureStorage(); - return storage.get(`encryptionKey-${userId}`, null); -}); - -/** - * Store user encryption key (OS-encrypted storage) - */ -ipcMain.handle('set-user-encryption-key', (_event, userId: string, encryptionKey: string) => { - const storage:SecureStorage = getSecureStorage(); - storage.set(`encryptionKey-${userId}`, encryptionKey); - return true; -}); - -/** - * Initialize database for user - */ -ipcMain.handle('db-initialize', (_event, userId: string, encryptionKey: string) => { - try { - const db:DatabaseService = getDatabaseService(); - db.initialize(userId, encryptionKey); - return { success: true }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error' - }; - } -}); - -/** - * Emergency restore - Clean up ALL local data - */ -function performEmergencyRestore(): void { - try { - // Close database connection - const db: DatabaseService = getDatabaseService(); - db.close(); - - // Get storage and userId before clearing - const storage: SecureStorage = getSecureStorage(); - const userId = storage.get('userId'); - const lastUserId = storage.get('lastUserId'); - - // Delete user-specific data - if (userId) { - storage.delete(`pin-${userId}`); - storage.delete(`encryptionKey-${userId}`); - } - if (lastUserId && lastUserId !== userId) { - storage.delete(`pin-${lastUserId}`); - storage.delete(`encryptionKey-${lastUserId}`); - } - - // Delete all general data - storage.delete('authToken'); - storage.delete('userId'); - storage.delete('lastUserId'); - storage.delete('userLang'); - storage.delete('offlineMode'); - storage.delete('syncInterval'); - - // Save cleared storage - storage.save(); - - // Delete database file - const userDataPath: string = app.getPath('userData'); - const dbPath: string = path.join(userDataPath, 'eritors-local.db'); - if (fs.existsSync(dbPath)) { - fs.unlinkSync(dbPath); - } - - // Delete secure config file to ensure complete reset - const secureConfigPath: string = path.join(userDataPath, 'secure-config.json'); - if (fs.existsSync(secureConfigPath)) { - fs.unlinkSync(secureConfigPath); - } - - console.log('[Emergency Restore] All local data cleared successfully'); - } catch (error) { - console.error('[Emergency Restore] Error:', error); - } - - // Restart app - if (mainWindow) { - mainWindow.close(); - mainWindow = null; - } - if (loginWindow) { - loginWindow.close(); - loginWindow = null; - } - - app.relaunch(); - app.exit(0); -} - -app.whenReady().then(():void => { - // Security: Disable web cache in production - if (!isDev) { - app.commandLine.appendSwitch('disable-http-cache'); - } - - // Security: Set permissions request handler - app.on('web-contents-created', (_event, contents) => { - // Allow only clipboard permissions, block others - contents.session.setPermissionRequestHandler((_webContents, permission, callback) => { - const allowedPermissions: string[] = ['clipboard-read', 'clipboard-sanitized-write']; - callback(allowedPermissions.includes(permission)); - }); - - // Block all web requests to file:// protocol - contents.session.protocol.interceptFileProtocol('file', (request, callback) => { - callback({ error: -3 }); // net::ERR_ABORTED - }); - }); - - // Menu minimal pour garder les raccourcis DevTools et clipboard - const template: Electron.MenuItemConstructorOptions[] = [ - { - label: 'Edit', - submenu: [ - { role: 'undo' }, - { role: 'redo' }, - { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, - { role: 'selectAll' } - ] - }, - { - label: 'View', - submenu: [ - { role: 'toggleDevTools' } - ] - }, - { - label: 'Help', - submenu: [ - { - label: 'Restore App', - click: () => { - dialog.showMessageBox({ - type: 'warning', - buttons: ['Cancel', 'Restore'], - defaultId: 0, - cancelId: 0, - title: 'Restore App', - message: 'Are you sure you want to restore the app?', - detail: 'This will delete all local data including: PIN codes, encryption keys, local database, and authentication tokens. The app will restart after restoration.' - }).then((result) => { - if (result.response === 1) { - performEmergencyRestore(); - } - }); - } - } - ] - } - ]; - - const menu = Menu.buildFromTemplate(template); - Menu.setApplicationMenu(menu); - - if (!isDev) { - const outPath:string = path.join(process.resourcesPath, 'app.asar.unpacked/out'); - - protocol.handle('scribedesktop', async (request) => { - // Security: Validate and sanitize file path - let filePath:string = request.url.replace('scribedesktop://', '').replace(/^\.\//, ''); - - // Security: Block path traversal attempts - if (filePath.includes('..') || filePath.includes('~')) { - console.error('[Security] Path traversal attempt blocked:', filePath); - return new Response('Forbidden', { status: 403 }); - } - - const fullPath:string = path.normalize(path.join(outPath, filePath)); - - // Security: Ensure path is within allowed directory - if (!fullPath.startsWith(outPath)) { - console.error('[Security] Path escape attempt blocked:', fullPath); - return new Response('Forbidden', { status: 403 }); - } - - try { - const data = await fs.promises.readFile(fullPath); - const ext:string = path.extname(fullPath).toLowerCase(); - const mimeTypes: Record = { - '.html': 'text/html', - '.css': 'text/css', - '.js': 'application/javascript', - '.json': 'application/json', - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.svg': 'image/svg+xml', - '.ico': 'image/x-icon', - '.woff': 'font/woff', - '.woff2': 'font/woff2', - '.ttf': 'font/ttf', - }; - - return new Response(data, { - headers: { - 'Content-Type': mimeTypes[ext] || 'application/octet-stream', - 'X-Content-Type-Options': 'nosniff' - } - }); - } catch (error) { - return new Response('Not found', { status: 404 }); - } - }); - } - - // Définir l'icône du Dock sur macOS - if (process.platform === 'darwin' && app.dock) { - const icon = nativeImage.createFromPath(iconPath); - app.dock.setIcon(icon); - } - - // Vérifier si un token existe (OS-encrypted storage) - const storage: SecureStorage = getSecureStorage(); - const token: string | null = storage.get('authToken'); - - if (token) { - createMainWindow(); - } else { - createLoginWindow(); - } - - app.on('activate', ():void => { - if (BrowserWindow.getAllWindows().length === 0) { - const storage: SecureStorage = getSecureStorage(); - const token: string | null = storage.get('authToken'); - if (token) { - createMainWindow(); - } else { - createLoginWindow(); - } - } - }); -}); - -app.on('window-all-closed', ():void => { - app.quit(); -}); diff --git a/electron/preload.ts b/electron/preload.ts deleted file mode 100644 index 0285a41..0000000 --- a/electron/preload.ts +++ /dev/null @@ -1,52 +0,0 @@ -const { contextBridge, ipcRenderer } = require('electron'); - -/** - * Exposer des APIs sécurisées au renderer process - * Utilise invoke() générique pour tous les appels IPC - */ -contextBridge.exposeInMainWorld('electron', { - // Platform info - platform: process.platform, - - // Generic invoke method - use this for all IPC calls - invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args), - - // Token management (shortcuts for convenience) - getToken: () => ipcRenderer.invoke('get-token'), - setToken: (token: string) => ipcRenderer.invoke('set-token', token), - removeToken: () => ipcRenderer.invoke('remove-token'), - - // Language management (shortcuts for convenience) - getLang: () => ipcRenderer.invoke('get-lang'), - setLang: (lang: 'fr' | 'en') => ipcRenderer.invoke('set-lang', lang), - - // Auth events (use send for one-way communication) - loginSuccess: (token: string) => ipcRenderer.send('login-success', token), - logout: () => ipcRenderer.send('logout'), - - // User initialization (after getting user info from server) - initUser: (userId: string) => ipcRenderer.invoke('init-user', userId), - - // Encryption key management (shortcuts for convenience) - generateEncryptionKey: (userId: string) => ipcRenderer.invoke('generate-encryption-key', userId), - getUserEncryptionKey: (userId: string) => ipcRenderer.invoke('get-user-encryption-key', userId), - setUserEncryptionKey: (userId: string, encryptionKey: string) => ipcRenderer.invoke('set-user-encryption-key', userId, encryptionKey), - - // Database initialization (shortcut for convenience) - dbInitialize: (userId: string, encryptionKey: string) => ipcRenderer.invoke('db-initialize', userId, encryptionKey), - - // Open external links (browser/native app) - openExternal: (url: string) => ipcRenderer.invoke('open-external', url), - - // OAuth login via BrowserWindow - oauthLogin: (provider: 'google' | 'facebook' | 'apple', baseUrl: string) => - ipcRenderer.invoke('oauth-login', { provider, baseUrl }), - - // Offline mode management - offlinePinSet: (pin: string) => ipcRenderer.invoke('offline:pin:set', { pin }), - offlinePinVerify: (pin: string) => ipcRenderer.invoke('offline:pin:verify', { pin }), - offlineModeSet: (enabled: boolean, syncInterval?: number) => - ipcRenderer.invoke('offline:mode:set', { enabled, syncInterval }), - offlineModeGet: () => ipcRenderer.invoke('offline:mode:get'), - offlineSyncCheck: () => ipcRenderer.invoke('offline:sync:check'), -}); diff --git a/electron/storage/SecureStorage.ts b/electron/storage/SecureStorage.ts deleted file mode 100644 index 8f8d405..0000000 --- a/electron/storage/SecureStorage.ts +++ /dev/null @@ -1,229 +0,0 @@ -import { safeStorage, app } from 'electron'; -import * as fs from 'fs'; -import * as path from 'path'; - -/** - * SecureStorage - Replacement for electron-store using Electron's safeStorage API - * - * Uses OS-level encryption: - * - macOS: Keychain - * - Windows: DPAPI (Data Protection API) - * - Linux: gnome-libsecret/kwallet - * - * Security notes: - * - Protects against physical theft (when PC is off) - * - Protects against other users on same machine - * - Does NOT protect against malware running under same user - * - On Linux, check getStorageBackend() - if 'basic_text', encryption is weak - */ - -type StorageValue = string; -type StoredData = Record; - -class SecureStorage { - private readonly storePath: string; - private readonly cache: Map = new Map(); - private isLoaded: boolean = false; - - constructor() { - const userDataPath: string = app.getPath('userData'); - this.storePath = path.join(userDataPath, 'secure-config.json'); - } - - /** - * Ensure data is loaded from disk (lazy loading) - */ - private ensureLoaded(): void { - if (!this.isLoaded) { - this.loadFromDisk(); - this.isLoaded = true; - } - } - - /** - * Load encrypted data from disk into memory cache - */ - private loadFromDisk(): void { - try { - if (!fs.existsSync(this.storePath)) { - return; - } - - const fileData: string = fs.readFileSync(this.storePath, 'utf-8'); - const parsed: unknown = JSON.parse(fileData); - - if (typeof parsed !== 'object' || parsed === null) { - console.error('[SecureStorage] Invalid data format in storage file'); - return; - } - - for (const [key, unknownValue] of Object.entries(parsed)) { - if (typeof unknownValue !== 'string' || unknownValue.length === 0) { - continue; - } - - const storedValue: string = unknownValue; - - try { - if (storedValue.startsWith('encrypted:')) { - const encryptedBase64: string = storedValue.substring('encrypted:'.length); - const buffer: Buffer = Buffer.from(encryptedBase64, 'base64'); - const decrypted: string = safeStorage.decryptString(buffer); - this.cache.set(key, decrypted); - } else if (storedValue.startsWith('plain:')) { - const plainValue: string = storedValue.substring('plain:'.length); - this.cache.set(key, plainValue); - } else { - try { - const buffer: Buffer = Buffer.from(storedValue, 'base64'); - const decrypted: string = safeStorage.decryptString(buffer); - this.cache.set(key, decrypted); - } catch (decryptError: unknown) { - this.cache.set(key, storedValue); - } - } - } catch (error: unknown) { - const errorMessage: string = error instanceof Error ? error.message : 'Unknown error'; - console.error(`[SecureStorage] Failed to load key '${key}': ${errorMessage}`); - } - } - } catch (error: unknown) { - const errorMessage: string = error instanceof Error ? error.message : 'Unknown error'; - console.error(`[SecureStorage] Failed to load from disk: ${errorMessage}`); - } - } - - /** - * Save encrypted data from memory cache to disk - */ - private saveToDisk(): void { - if (!safeStorage.isEncryptionAvailable()) { - throw new Error('Encryption not available - cannot save securely'); - } - - const data: StoredData = {}; - - for (const [key, value] of this.cache.entries()) { - if (!value) { - throw new Error(`Invalid value for key '${key}'`); - } - - const buffer: Buffer = safeStorage.encryptString(value); - if (!buffer || buffer.length === 0) { - throw new Error(`Failed to encrypt key '${key}'`); - } - data[key] = `encrypted:${buffer.toString('base64')}`; - } - - try { - const dir: string = path.dirname(this.storePath); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - } - - fs.writeFileSync(this.storePath, JSON.stringify(data, null, 2), 'utf-8'); - } catch (error: unknown) { - const errorMessage: string = error instanceof Error ? error.message : 'Unknown error'; - console.error(`[SecureStorage] Failed to save to disk: ${errorMessage}`); - throw error; - } - } - - /** - * Get a value from secure storage - * @param key - Storage key - * @param defaultValue - Default value if key doesn't exist - * @returns Stored value or default - */ - public get(key: string, defaultValue: T | null = null): T | null { - this.ensureLoaded(); - const value: StorageValue | undefined = this.cache.get(key); - - if (value === undefined) { - return defaultValue; - } - - try { - return JSON.parse(value) as T; - } catch { - return value as unknown as T; - } - } - - /** - * Set a value in secure storage (kept in memory only) - * @param key - Storage key - * @param value - Value to store - */ - public set(key: string, value: unknown): void { - this.ensureLoaded(); - const stringValue: string = typeof value === 'string' ? value : JSON.stringify(value); - this.cache.set(key, stringValue); - } - - /** - * Delete a value from secure storage (memory only) - * @param key - Storage key - */ - public delete(key: string): void { - this.ensureLoaded(); - this.cache.delete(key); - } - - /** - * Check if a key exists in secure storage - * @param key - Storage key - * @returns True if key exists - */ - public has(key: string): boolean { - this.ensureLoaded(); - return this.cache.has(key); - } - - /** - * Clear all data from secure storage (memory only) - */ - public clear(): void { - this.cache.clear(); - } - - /** - * Manually save to disk (encrypted with safeStorage) - * Call this when you want to persist data - */ - public save(): void { - this.saveToDisk(); - } - - /** - * Check if encryption is available - * @returns True if OS-level encryption is available - */ - public isEncryptionAvailable(): boolean { - return safeStorage.isEncryptionAvailable(); - } -} - -declare global { - var __secureStorageInstance: SecureStorage | undefined; -} - -/** - * Get the SecureStorage singleton instance - * @returns SecureStorage instance - */ -export function getSecureStorage(): SecureStorage { - if (!global.__secureStorageInstance) { - global.__secureStorageInstance = new SecureStorage(); - - if (!global.__secureStorageInstance.isEncryptionAvailable()) { - console.warn( - '[SecureStorage] WARNING: OS-level encryption is not available. ' + - 'Data will still be stored but with reduced security.' - ); - } - } - return global.__secureStorageInstance; -} - -export default SecureStorage; diff --git a/index.html b/index.html new file mode 100644 index 0000000..540e8c8 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + ERitors Scribe + + + +
    + + + diff --git a/lib/api/client.ts b/lib/api/client.ts index c173e9a..f85ed98 100644 --- a/lib/api/client.ts +++ b/lib/api/client.ts @@ -1,104 +1,93 @@ -import axios, {AxiosResponse, Method} from "axios"; -import {configs, isDesktop} from "@/lib/configs"; - -type ContentType = 'application/json' | 'multipart/form-data'; - -interface ApiRequestConfig { - method: Method; - url: string; - auth: string; - lang?: string; - params?: Record; - data?: unknown; - contentType?: ContentType; -} - -export class ApiError extends Error { - statusCode: number; - constructor(message: string, statusCode: number) { - super(message); - this.statusCode = statusCode; - this.name = 'ApiError'; - } -} - -function handleApiError(error: unknown): never { - if (axios.isAxiosError(error)) { - const serverMessage: string = error.response?.data?.message || error.response?.data || error.message; - const statusCode: number = error.response?.status ?? 500; - throw new ApiError(serverMessage, statusCode); - } else if (error instanceof Error) { - throw new Error(error.message); - } - throw new Error('An unexpected error occurred'); -} - -async function apiRequest(config: ApiRequestConfig): Promise { - try { - const headers: Record = { - 'Authorization': `Bearer ${config.auth}` - }; - - if (config.contentType) { - headers['Content-Type'] = config.contentType; - } - - const response: AxiosResponse = await axios({ - method: config.method, - headers, - params: { - lang: config.lang ?? 'fr', - plateforme: isDesktop ? 'desktop' : 'web', - ...config.params - }, - url: configs.apiUrl + config.url, - data: config.data - }); - - return response.data; - } catch (error: unknown) { - handleApiError(error); - } -} - -export async function apiGet(url: string, auth: string, lang: string = "fr", params: Record = {}): Promise { - return apiRequest({method: 'GET', url, auth, lang, params}); -} - -export async function apiPost(url: string, data: object, auth: string, lang: string = "fr"): Promise { - return apiRequest({method: 'POST', url, auth, lang, data, contentType: 'application/json'}); -} - -export async function apiPut(url: string, data: object, auth: string, lang: string = "fr"): Promise { - return apiRequest({method: 'PUT', url, auth, lang, data, contentType: 'application/json'}); -} - -export async function apiPatch(url: string, data: object, auth: string, lang: string = "fr"): Promise { - return apiRequest({method: 'PATCH', url, auth, lang, data, contentType: 'application/json'}); -} - -export async function apiDelete(url: string, data: object, auth: string, lang: string = "fr"): Promise { - return apiRequest({method: 'DELETE', url, auth, lang, data, contentType: 'application/json'}); -} - -export async function apiPostPublic(url: string, data: object, lang: string = "fr"): Promise { - try { - const response: AxiosResponse = await axios({ - method: 'POST', - headers: {'Content-Type': 'application/json'}, - params: {lang, plateforme: isDesktop ? 'desktop' : 'web'}, - url: configs.apiUrl + url, - data - }); - return response.data; - } catch (error: unknown) { - handleApiError(error); - } -} - -export async function apiUpload(url: string, file: File, auth: string, lang: string = "fr"): Promise { - const formData: FormData = new FormData(); - formData.append('file', file); - - return apiRequest({method: 'POST', url, auth, lang, data: formData, contentType: 'multipart/form-data'}); -} +import {fetch} from "@tauri-apps/plugin-http"; +import {configs} from "@/lib/configs"; + +export class ApiError extends Error { + statusCode: number; + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + this.name = 'ApiError'; + } +} + +function buildUrl(url: string, params: Record = {}, lang: string = "fr"): string { + const fullUrl = new URL(url, configs.apiUrl); + fullUrl.searchParams.set("lang", lang); + fullUrl.searchParams.set("plateforme", "desktop"); + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null) fullUrl.searchParams.set(key, String(value)); + } + return fullUrl.toString(); +} + +async function handleResponse(response: Response): Promise { + if (!response.ok) { + const body = await response.json().catch(() => ({message: response.statusText})); + throw new ApiError(body.message || body || response.statusText, response.status); + } + return response.json() as Promise; +} + +export async function apiGet(url: string, auth: string, lang: string = "fr", params: Record = {}): Promise { + const response = await fetch(buildUrl(url, params, lang), { + method: "GET", + headers: {"Authorization": `Bearer ${auth}`}, + }); + return handleResponse(response); +} + +export async function apiPost(url: string, data: object, auth: string, lang: string = "fr"): Promise { + const response = await fetch(buildUrl(url, {}, lang), { + method: "POST", + headers: {"Authorization": `Bearer ${auth}`, "Content-Type": "application/json"}, + body: JSON.stringify(data), + }); + return handleResponse(response); +} + +export async function apiPut(url: string, data: object, auth: string, lang: string = "fr"): Promise { + const response = await fetch(buildUrl(url, {}, lang), { + method: "PUT", + headers: {"Authorization": `Bearer ${auth}`, "Content-Type": "application/json"}, + body: JSON.stringify(data), + }); + return handleResponse(response); +} + +export async function apiPatch(url: string, data: object, auth: string, lang: string = "fr"): Promise { + const response = await fetch(buildUrl(url, {}, lang), { + method: "PATCH", + headers: {"Authorization": `Bearer ${auth}`, "Content-Type": "application/json"}, + body: JSON.stringify(data), + }); + return handleResponse(response); +} + +export async function apiDelete(url: string, data: object, auth: string, lang: string = "fr"): Promise { + const response = await fetch(buildUrl(url, {}, lang), { + method: "DELETE", + headers: {"Authorization": `Bearer ${auth}`, "Content-Type": "application/json"}, + body: JSON.stringify(data), + }); + return handleResponse(response); +} + +export async function apiPostPublic(url: string, data: object, lang: string = "fr"): Promise { + const response = await fetch(buildUrl(url, {}, lang), { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(data), + }); + return handleResponse(response); +} + +export async function apiUpload(url: string, file: File, auth: string, lang: string = "fr"): Promise { + const formData = new FormData(); + formData.append("file", file); + const response = await fetch(buildUrl(url, {}, lang), { + method: "POST", + headers: {"Authorization": `Bearer ${auth}`}, + body: formData, + }); + return handleResponse(response); +} diff --git a/lib/configs.ts b/lib/configs.ts index 97e904f..54c67a6 100644 --- a/lib/configs.ts +++ b/lib/configs.ts @@ -8,7 +8,7 @@ export interface Configs { appVersion: string; } -const isProduction: boolean = false; +const isProduction: boolean = true; export const isDesktop: boolean = true; export const configs: Configs = { diff --git a/lib/crashReporter.ts b/lib/crashReporter.ts index 56ed24b..bf158c3 100644 --- a/lib/crashReporter.ts +++ b/lib/crashReporter.ts @@ -1,4 +1,4 @@ -import axios from 'axios'; +import {fetch} from '@tauri-apps/plugin-http'; import {configs, isDesktop} from '@/lib/configs'; interface CrashReportPayload { @@ -49,9 +49,10 @@ function getUserId(): string | undefined { async function sendCrashReport(payload: CrashReportPayload): Promise { try { - await axios.post(configs.apiUrl + 'crash-report', payload, { + await fetch(configs.apiUrl + 'crash-report', { + method: 'POST', headers: {'Content-Type': 'application/json'}, - timeout: 5000, + body: JSON.stringify(payload), }); } catch { // Silently fail — we can't crash while reporting a crash diff --git a/lib/locales/en.json b/lib/locales/en.json index d669283..b650194 100644 --- a/lib/locales/en.json +++ b/lib/locales/en.json @@ -280,7 +280,6 @@ "emptyDescription": "Search for ideas to enrich your writing. Enter a prompt and let AI inspire you with creative suggestions based on your current content.", "emptyPromptError": "Please enter a prompt to get inspired.", "error": { - "contentRetrieval": "Error retrieving content.", "contentRetrievalUnknown": "Unknown error retrieving content.", "noBook": "No book selected.", "noChapter": "No chapter selected.", @@ -669,7 +668,6 @@ "successSaveAdvanced": "Advanced settings saved successfully.", "errorSave": "An error occurred during saving.", "errorUnknownSave": "An unknown error occurred during saving.", - "errorRetrieveContent": "Error retrieving content.", "errorUnknownRetrieveContent": "Unknown error retrieving content.", "abortSuccess": "Generation stopped. Token and cost totals will be available on next page refresh.", "settings": { @@ -1286,7 +1284,6 @@ "emailLength": "Email address must be between 5 and 100 characters.", "emailInvalidChars": "Email address contains invalid characters.", "connection": "Connection error. Check your credentials.", - "server": "Server error. Please try again later.", "unknown": "An unknown error occurred." } }, @@ -1402,11 +1399,8 @@ "error": { "emailInvalid": "Please enter a valid email address.", "emailFormat": "The email address format is invalid.", - "emailServer": "Server error while verifying email.", "emailUnknown": "An unknown error occurred.", - "codeServer": "Server error while verifying code.", "codeUnknown": "An unknown error occurred.", - "passwordServer": "Server error while changing password.", "passwordUnknown": "An unknown error occurred." } }, diff --git a/lib/locales/fr.json b/lib/locales/fr.json index ec4f097..047579d 100644 --- a/lib/locales/fr.json +++ b/lib/locales/fr.json @@ -280,7 +280,6 @@ "emptyDescription": "Recherchez des idées pour enrichir votre écriture. Entrez un prompt et laissez l'IA vous inspirer avec des suggestions créatives basées sur votre contenu actuel.", "emptyPromptError": "Veuillez entrer un prompt pour vous inspirer.", "error": { - "contentRetrieval": "Erreur lors de la récupération du contenu.", "contentRetrievalUnknown": "Erreur inconnue lors de la récupération du contenu.", "noBook": "Aucun livre sélectionné.", "noChapter": "Aucun chapitre sélectionné.", @@ -669,7 +668,6 @@ "successSaveAdvanced": "Paramètres avancés sauvegardés avec succès.", "errorSave": "Une erreur est survenue pendant la sauvegarde.", "errorUnknownSave": "Une erreur inconnue est survenue pendant la sauvegarde.", - "errorRetrieveContent": "Erreur lors de la récupération du contenu.", "errorUnknownRetrieveContent": "Erreur inconnue lors de la récupération du contenu.", "abortSuccess": "Génération arrêtée. Les totaux de tokens et coûts seront disponibles au prochain rafraîchissement de la page.", "settings": { @@ -1285,7 +1283,6 @@ "emailLength": "L'adresse e-mail doit contenir entre 5 et 100 caractères.", "emailInvalidChars": "L'adresse e-mail contient des caractères invalides.", "connection": "Erreur de connexion. Vérifiez vos identifiants.", - "server": "Erreur du serveur. Veuillez réessayer plus tard.", "unknown": "Une erreur inconnue est survenue." } }, @@ -1401,11 +1398,8 @@ "error": { "emailInvalid": "Veuillez entrer une adresse e-mail valide.", "emailFormat": "Le format de l'adresse e-mail est invalide.", - "emailServer": "Erreur du serveur lors de la vérification de l'e-mail.", "emailUnknown": "Une erreur inconnue est survenue.", - "codeServer": "Erreur du serveur lors de la vérification du code.", "codeUnknown": "Une erreur inconnue est survenue.", - "passwordServer": "Erreur du serveur lors du changement de mot de passe.", "passwordUnknown": "Une erreur inconnue est survenue." } }, diff --git a/next.config.ts b/next.config.ts deleted file mode 100644 index 5ceb797..0000000 --- a/next.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { NextConfig } from 'next'; - -const nextConfig: NextConfig = { - output: 'export', - images: { - unoptimized: true, - }, - trailingSlash: true, - typescript: { - ignoreBuildErrors: true, - }, -}; - -export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 573ba70..9165993 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "eritorsscribe", - "version": "0.4.1", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "eritorsscribe", - "version": "0.4.1", + "version": "0.5.0", "license": "ISC", "dependencies": { "@tailwindcss/postcss": "^4.1.17", "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-http": "^2.5.8", "@tauri-apps/plugin-shell": "^2.3.5", "@tiptap/extension-color": "^3.10.7", "@tiptap/extension-gapcursor": "^3.10.7", @@ -19,17 +20,12 @@ "@tiptap/extension-underline": "^3.10.7", "@tiptap/react": "^3.10.7", "@tiptap/starter-kit": "^3.10.7", - "@types/bcrypt": "^6.0.0", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.4.22", - "axios": "^1.13.2", - "bcrypt": "^6.0.0", "docx": "^9.5.3", - "electron-updater": "^6.7.3", "i18next": "^25.10.4", "jszip": "^3.10.1", "lucide-react": "^0.577.0", - "node-sqlite3-wasm": "^0.8.51", "pdfkit": "^0.17.2", "postcss": "^8.5.6", "react": "^19.2.0", @@ -40,7 +36,6 @@ "vite": "^8.0.1" }, "devDependencies": { - "@electron/notarize": "^3.1.1", "@tauri-apps/cli": "^2.10.1", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.10.1", @@ -49,8 +44,7 @@ "@types/react-dom": "^19.2.3", "concurrently": "^9.2.1", "dotenv": "^17.2.3", - "electron": "^39.2.1", - "electron-builder": "^26.0.12", + "dotenv-cli": "^11.0.0", "typescript": "^5.9.3", "wait-on": "^9.0.3" } @@ -76,408 +70,6 @@ "node": ">=6.9.0" } }, - "node_modules/@develar/schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/@electron/asar": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.4.1.tgz", - "integrity": "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^5.0.0", - "glob": "^7.1.6", - "minimatch": "^3.0.4" - }, - "bin": { - "asar": "bin/asar.js" - }, - "engines": { - "node": ">=10.12.0" - } - }, - "node_modules/@electron/asar/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@electron/fuses": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@electron/fuses/-/fuses-1.8.0.tgz", - "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.1", - "fs-extra": "^9.0.1", - "minimist": "^1.2.5" - }, - "bin": { - "electron-fuses": "dist/bin.js" - } - }, - "node_modules/@electron/fuses/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/fuses/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/fuses/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/get": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", - "integrity": "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/@electron/notarize": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-3.1.1.tgz", - "integrity": "sha512-uQQSlOiJnqRkTL1wlEBAxe90nVN/Fc/hEmk0bqpKk8nKjV1if/tXLHKUPePtv9Xsx90PtZU8aidx5lAiOpjkQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 22.12.0" - } - }, - "node_modules/@electron/osx-sign": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@electron/osx-sign/-/osx-sign-1.3.3.tgz", - "integrity": "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "compare-version": "^0.1.2", - "debug": "^4.3.4", - "fs-extra": "^10.0.0", - "isbinaryfile": "^4.0.8", - "minimist": "^1.2.6", - "plist": "^3.0.5" - }, - "bin": { - "electron-osx-flat": "bin/electron-osx-flat.js", - "electron-osx-sign": "bin/electron-osx-sign.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/@electron/osx-sign/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@electron/osx-sign/node_modules/isbinaryfile": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", - "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/@electron/osx-sign/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/osx-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/rebuild": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", - "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.1.1", - "detect-libc": "^2.0.1", - "got": "^11.7.0", - "graceful-fs": "^4.2.11", - "node-abi": "^4.2.0", - "node-api-version": "^0.2.1", - "node-gyp": "^11.2.0", - "ora": "^5.1.0", - "read-binary-file-arch": "^1.0.6", - "semver": "^7.3.5", - "tar": "^7.5.6", - "yargs": "^17.0.1" - }, - "bin": { - "electron-rebuild": "lib/cli.js" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/@electron/rebuild/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@electron/universal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.3.tgz", - "integrity": "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@electron/asar": "^3.3.1", - "@malept/cross-spawn-promise": "^2.0.0", - "debug": "^4.3.1", - "dir-compare": "^4.2.0", - "fs-extra": "^11.1.1", - "minimatch": "^9.0.3", - "plist": "^3.1.0" - }, - "engines": { - "node": ">=16.4" - } - }, - "node_modules/@electron/universal/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@electron/universal/node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/universal/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/universal/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@electron/universal/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@electron/windows-sign": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz", - "integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "peer": true, - "dependencies": { - "cross-dirname": "^0.1.0", - "debug": "^4.3.4", - "fs-extra": "^11.1.1", - "minimist": "^1.2.8", - "postject": "^1.0.0-alpha.6" - }, - "bin": { - "electron-windows-sign": "bin/electron-windows-sign.js" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/fs-extra": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", - "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/@electron/windows-sign/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@electron/windows-sign/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/@emnapi/core": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", @@ -591,145 +183,6 @@ "@hapi/hoek": "^11.0.2" } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", - "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -775,84 +228,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@malept/cross-spawn-promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", - "integrity": "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/malept" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" - } - ], - "license": "Apache-2.0", - "dependencies": { - "cross-spawn": "^7.0.1" - }, - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/@malept/flatpak-bundler": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@malept/flatpak-bundler/-/flatpak-bundler-0.4.0.tgz", - "integrity": "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.0", - "lodash": "^4.17.15", - "tmp-promise": "^3.0.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/@malept/flatpak-bundler/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", @@ -869,56 +244,6 @@ "url": "https://github.com/sponsors/Brooooooklyn" } }, - "node_modules/@npmcli/agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", - "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/agent/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@npmcli/fs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", - "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", - "dev": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@oxc-project/types": { "version": "0.120.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", @@ -928,17 +253,6 @@ "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@remirror/core-constants": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", @@ -1191,19 +505,6 @@ "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@standard-schema/spec": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", @@ -1220,19 +521,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@tailwindcss/node": { "version": "4.1.18", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", @@ -1716,6 +1004,15 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-http": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.5.8.tgz", + "integrity": "sha512-oxd7oypzQeu8kAfFCrw534Kq7Cw+NzozcnCY21O4rz3A+veJiIiuSCMIprgGcZOcLAXFP9GmDhKUbhuKWcunRw==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, "node_modules/@tauri-apps/plugin-shell": { "version": "2.3.5", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz", @@ -2222,55 +1519,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@types/bcrypt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", - "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/fs-extra": { - "version": "9.0.13", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", - "integrity": "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/jsonwebtoken": { "version": "9.0.10", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", @@ -2282,16 +1530,6 @@ "@types/node": "*" } }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -2325,6 +1563,7 @@ "version": "24.10.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.12.tgz", "integrity": "sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==", + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" @@ -2340,18 +1579,6 @@ "@types/node": "*" } }, - "node_modules/@types/plist": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/plist/-/plist-3.0.5.tgz", - "integrity": "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*", - "xmlbuilder": ">=11.0.1" - } - }, "node_modules/@types/react": { "version": "19.2.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", @@ -2370,41 +1597,12 @@ "@types/react": "^19.2.0" } }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" }, - "node_modules/@types/verror": { - "version": "1.10.11", - "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.11.tgz", - "integrity": "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@vitejs/plugin-react": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", @@ -2430,70 +1628,6 @@ } } }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.11", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", - "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/7zip-bin": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", - "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", - "dev": true, - "license": "MIT" - }, - "node_modules/abbrev": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", - "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -2520,306 +1654,18 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/app-builder-bin": { - "version": "5.0.0-alpha.12", - "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz", - "integrity": "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/app-builder-lib": { - "version": "26.7.0", - "resolved": "https://registry.npmjs.org/app-builder-lib/-/app-builder-lib-26.7.0.tgz", - "integrity": "sha512-/UgCD8VrO79Wv8aBNpjMfsS1pIUfIPURoRn0Ik6tMe5avdZF+vQgl/juJgipcMmH3YS0BD573lCdCHyoi84USg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@develar/schema-utils": "~2.6.5", - "@electron/asar": "3.4.1", - "@electron/fuses": "^1.8.0", - "@electron/get": "^3.0.0", - "@electron/notarize": "2.5.0", - "@electron/osx-sign": "1.3.3", - "@electron/rebuild": "^4.0.3", - "@electron/universal": "2.0.3", - "@malept/flatpak-bundler": "^0.4.0", - "@types/fs-extra": "9.0.13", - "async-exit-hook": "^2.0.1", - "builder-util": "26.4.1", - "builder-util-runtime": "9.5.1", - "chromium-pickle-js": "^0.2.0", - "ci-info": "4.3.1", - "debug": "^4.3.4", - "dotenv": "^16.4.5", - "dotenv-expand": "^11.0.6", - "ejs": "^3.1.8", - "electron-publish": "26.6.0", - "fs-extra": "^10.1.0", - "hosted-git-info": "^4.1.0", - "isbinaryfile": "^5.0.0", - "jiti": "^2.4.2", - "js-yaml": "^4.1.0", - "json5": "^2.2.3", - "lazy-val": "^1.0.5", - "minimatch": "^10.0.3", - "plist": "3.1.0", - "proper-lockfile": "^4.1.2", - "resedit": "^1.7.0", - "semver": "~7.7.3", - "tar": "^7.5.7", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0", - "which": "^5.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "dmg-builder": "26.7.0", - "electron-builder-squirrel-windows": "26.7.0" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@electron/get/-/get-3.1.0.tgz", - "integrity": "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "env-paths": "^2.2.0", - "fs-extra": "^8.1.0", - "got": "^11.8.5", - "progress": "^2.0.3", - "semver": "^6.2.0", - "sumchecker": "^3.0.1" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "global-agent": "^3.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/get/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/notarize": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@electron/notarize/-/notarize-2.5.0.tgz", - "integrity": "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.1.1", - "fs-extra": "^9.0.1", - "promise-retry": "^2.0.1" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/app-builder-lib/node_modules/@electron/notarize/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/ci-info": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", - "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/app-builder-lib/node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/app-builder-lib/node_modules/fs-extra/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/app-builder-lib/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/astral-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-exit-hook": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/async-exit-hook/-/async-exit-hook-2.0.1.tgz", - "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/at-least-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", - "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true, - "license": "ISC", - "engines": { - "node": ">= 4.0.0" - } + "license": "MIT" }, "node_modules/autoprefixer": { "version": "10.4.24", @@ -2861,6 +1707,7 @@ "version": "1.13.5", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -2868,13 +1715,6 @@ "proxy-from-env": "^1.1.0" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2904,52 +1744,6 @@ "baseline-browser-mapping": "dist/cli.js" } }, - "node_modules/bcrypt": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", - "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "node-addon-api": "^8.3.0", - "node-gyp-build": "^4.8.4" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/boolean": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", - "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/brotli": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.3.tgz", @@ -2992,236 +1786,11 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/builder-util": { - "version": "26.4.1", - "resolved": "https://registry.npmjs.org/builder-util/-/builder-util-26.4.1.tgz", - "integrity": "sha512-FlgH43XZ50w3UtS1RVGDWOz8v9qMXPC7upMtKMtBEnYdt1OVoS61NYhKm/4x+cIaWqJTXua0+VVPI+fSPGXNIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.6", - "7zip-bin": "~5.2.0", - "app-builder-bin": "5.0.0-alpha.12", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "cross-spawn": "^7.0.6", - "debug": "^4.3.4", - "fs-extra": "^10.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "js-yaml": "^4.1.0", - "sanitize-filename": "^1.6.3", - "source-map-support": "^0.5.19", - "stat-mode": "^1.0.0", - "temp-file": "^3.4.0", - "tiny-async-pool": "1.3.0" - } - }, - "node_modules/builder-util-runtime": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz", - "integrity": "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.4", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/builder-util/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/builder-util/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/builder-util/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/cacache": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", - "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^4.0.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/cacache/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3281,83 +1850,6 @@ "node": ">=8" } }, - "node_modules/chownr": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", - "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/chromium-pickle-js": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", - "integrity": "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==", - "dev": true, - "license": "MIT" - }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", - "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "slice-ansi": "^3.0.0", - "string-width": "^4.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3373,29 +1865,6 @@ "node": ">=12" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3420,6 +1889,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -3428,33 +1898,6 @@ "node": ">= 0.8" } }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/compare-version": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/compare-version/-/compare-version-0.1.2.tgz", - "integrity": "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/concurrently": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", @@ -3499,32 +1942,12 @@ "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "license": "MIT" }, - "node_modules/crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "buffer": "^5.1.0" - } - }, "node_modules/crelt": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", "license": "MIT" }, - "node_modules/cross-dirname": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz", - "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3575,117 +1998,11 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -3700,126 +2017,12 @@ "node": ">=8" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/dfa": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==", "license": "MIT" }, - "node_modules/dir-compare": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", - "integrity": "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimatch": "^3.0.5", - "p-limit": "^3.1.0 " - } - }, - "node_modules/dir-compare/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/dmg-builder": { - "version": "26.7.0", - "resolved": "https://registry.npmjs.org/dmg-builder/-/dmg-builder-26.7.0.tgz", - "integrity": "sha512-uOOBA3f+kW3o4KpSoMQ6SNpdXU7WtxlJRb9vCZgOvqhTz4b3GjcoWKstdisizNZLsylhTMv8TLHFPFW0Uxsj/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.7.0", - "builder-util": "26.4.1", - "fs-extra": "^10.1.0", - "iconv-lite": "^0.6.2", - "js-yaml": "^4.1.0" - }, - "optionalDependencies": { - "dmg-license": "^1.0.11" - } - }, - "node_modules/dmg-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/dmg-builder/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/dmg-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/dmg-license": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", - "integrity": "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "@types/plist": "^3.0.1", - "@types/verror": "^1.10.3", - "ajv": "^6.10.0", - "crc": "^3.8.0", - "iconv-corefoundation": "^1.1.7", - "plist": "^3.0.4", - "smart-buffer": "^4.0.2", - "verror": "^1.10.0" - }, - "bin": { - "dmg-license": "bin/dmg-license.js" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/docx": { "version": "9.5.3", "resolved": "https://registry.npmjs.org/docx/-/docx-9.5.3.tgz", @@ -3883,10 +2086,26 @@ "url": "https://dotenvx.com" } }, - "node_modules/dotenv-expand": { - "version": "11.0.7", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", - "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "node_modules/dotenv-cli": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-11.0.0.tgz", + "integrity": "sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6", + "dotenv": "^17.1.0", + "dotenv-expand": "^12.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "dotenv": "cli.js" + } + }, + "node_modules/dotenv-cli/node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3899,7 +2118,7 @@ "url": "https://dotenvx.com" } }, - "node_modules/dotenv-expand/node_modules/dotenv": { + "node_modules/dotenv-cli/node_modules/dotenv-expand/node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", @@ -3916,6 +2135,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3926,304 +2146,12 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ejs": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", - "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "jake": "^10.8.5" - }, - "bin": { - "ejs": "bin/cli.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/electron": { - "version": "39.5.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-39.5.1.tgz", - "integrity": "sha512-6s/sBQar+bbW59XSqohZj04MPic+kdVUAWjLbfQB/uLOeNw9jWX5FHaTxpHK29Xp3mKOHef7wErsjwMyCuWltg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@electron/get": "^2.0.0", - "@types/node": "^22.7.7", - "extract-zip": "^2.0.1" - }, - "bin": { - "electron": "cli.js" - }, - "engines": { - "node": ">= 12.20.55" - } - }, - "node_modules/electron-builder": { - "version": "26.7.0", - "resolved": "https://registry.npmjs.org/electron-builder/-/electron-builder-26.7.0.tgz", - "integrity": "sha512-LoXbCvSFxLesPneQ/fM7FB4OheIDA2tjqCdUkKlObV5ZKGhYgi5VHPHO/6UUOUodAlg7SrkPx7BZJPby+Vrtbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "app-builder-lib": "26.7.0", - "builder-util": "26.4.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "dmg-builder": "26.7.0", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "simple-update-notifier": "2.0.0", - "yargs": "^17.6.2" - }, - "bin": { - "electron-builder": "cli.js", - "install-app-deps": "install-app-deps.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/electron-builder-squirrel-windows": { - "version": "26.7.0", - "resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.7.0.tgz", - "integrity": "sha512-3EqkQK+q0kGshdPSKEPb2p5F75TENMKu6Fe5aTdeaPfdzFK4Yjp5L0d6S7K8iyvqIsGQ/ei4bnpyX9wt+kVCKQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "app-builder-lib": "26.7.0", - "builder-util": "26.4.1", - "electron-winstaller": "5.4.0" - } - }, - "node_modules/electron-builder/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-builder/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-builder/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-publish": { - "version": "26.6.0", - "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.6.0.tgz", - "integrity": "sha512-LsyHMMqbvJ2vsOvuWJ19OezgF2ANdCiHpIucDHNiLhuI+/F3eW98ouzWSRmXXi82ZOPZXC07jnIravY4YYwCLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/fs-extra": "^9.0.11", - "builder-util": "26.4.1", - "builder-util-runtime": "9.5.1", - "chalk": "^4.1.2", - "form-data": "^4.0.5", - "fs-extra": "^10.1.0", - "lazy-val": "^1.0.5", - "mime": "^2.5.2" - } - }, - "node_modules/electron-publish/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-publish/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-publish/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/electron-to-chromium": { "version": "1.5.286", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", "license": "ISC" }, - "node_modules/electron-updater": { - "version": "6.7.3", - "resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.7.3.tgz", - "integrity": "sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==", - "license": "MIT", - "dependencies": { - "builder-util-runtime": "9.5.1", - "fs-extra": "^10.1.0", - "js-yaml": "^4.1.0", - "lazy-val": "^1.0.5", - "lodash.escaperegexp": "^4.1.2", - "lodash.isequal": "^4.5.0", - "semver": "~7.7.3", - "tiny-typed-emitter": "^2.1.0" - } - }, - "node_modules/electron-updater/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/electron-updater/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/electron-updater/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/electron-updater/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/electron-winstaller": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz", - "integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@electron/asar": "^3.2.1", - "debug": "^4.1.1", - "fs-extra": "^7.0.1", - "lodash": "^4.17.21", - "temp": "^0.9.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "optionalDependencies": { - "@electron/windows-sign": "^1.1.2" - } - }, - "node_modules/electron-winstaller/node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/electron/node_modules/@types/node": { - "version": "22.19.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.10.tgz", - "integrity": "sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/electron/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -4231,27 +2159,6 @@ "dev": true, "license": "MIT" }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "dev": true, - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.19.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", @@ -4277,27 +2184,11 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true, - "license": "MIT" - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4307,6 +2198,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4316,6 +2208,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -4328,6 +2221,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4339,14 +2233,6 @@ "node": ">= 0.4" } }, - "node_modules/es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4368,45 +2254,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/exponential-backoff": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT", - "optional": true - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4422,23 +2269,6 @@ "node": ">=6.0.0" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4456,43 +2286,11 @@ } } }, - "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "minimatch": "^5.0.1" - } - }, - "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, "funding": [ { "type": "individual", @@ -4535,40 +2333,11 @@ "node": ">=0.8" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -4594,41 +2363,6 @@ "url": "https://github.com/sponsors/rawify" } }, - "node_modules/fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-minipass": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", - "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4647,6 +2381,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4666,6 +2401,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4690,6 +2426,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -4699,112 +2436,11 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/global-agent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", - "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "es6-error": "^4.1.1", - "matcher": "^3.0.0", - "roarr": "^2.15.3", - "semver": "^7.3.2", - "serialize-error": "^7.0.1" - }, - "engines": { - "node": ">=10.0" - } - }, - "node_modules/global-agent/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "optional": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4813,32 +2449,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4855,24 +2465,11 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4885,6 +2482,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -4910,6 +2508,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4918,19 +2517,6 @@ "node": ">= 0.4" } }, - "node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/html-parse-stringify": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", @@ -4940,55 +2526,6 @@ "void-elements": "3.1.0" } }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/i18next": { "version": "25.10.4", "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.10.4.tgz", @@ -5020,110 +2557,18 @@ } } }, - "node_modules/iconv-corefoundation": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", - "integrity": "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==", - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "cli-truncate": "^2.1.0", - "node-addon-api": "^1.6.3" - }, - "engines": { - "node": "^8.11.2 || >=10" - } - }, - "node_modules/iconv-corefoundation/node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -5134,92 +2579,12 @@ "node": ">=8" } }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "license": "MIT" }, - "node_modules/isbinaryfile": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", - "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/gjtorikian/" - } - }, - "node_modules/isexe": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", - "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jake": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", - "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.6", - "filelist": "^1.0.4", - "picocolors": "^1.1.1" - }, - "bin": { - "jake": "bin/cli.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -5255,63 +2620,6 @@ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true, - "license": "ISC", - "optional": true - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -5354,22 +2662,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/lazy-val": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", - "integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==", - "license": "MIT" - }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -5669,59 +2961,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.escaperegexp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "license": "MIT" - }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT" - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/lucide-react": { "version": "0.577.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", @@ -5740,29 +2979,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/make-fetch-happen": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", - "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", - "http-cache-semantics": "^4.1.1", - "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^1.0.0", - "proc-log": "^5.0.0", - "promise-retry": "^2.0.1", - "ssri": "^12.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", @@ -5780,24 +2996,11 @@ "markdown-it": "bin/markdown-it.mjs" } }, - "node_modules/matcher": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", - "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "escape-string-regexp": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5809,23 +3012,11 @@ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "license": "MIT" }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -5835,6 +3026,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -5843,48 +3035,12 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "license": "ISC" }, - "node_modules/minimatch": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.2.tgz", - "integrity": "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.1" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", @@ -5895,158 +3051,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-collect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", - "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/minipass-fetch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", - "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-pipeline/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized/node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", - "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.1.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -6065,293 +3069,24 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "4.26.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", - "integrity": "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.6.3" - }, - "engines": { - "node": ">=22.12.0" - } - }, - "node_modules/node-abi/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", - "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", - "license": "MIT", - "engines": { - "node": "^18 || ^20 || >= 21" - } - }, - "node_modules/node-api-version": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", - "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - } - }, - "node_modules/node-api-version/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-gyp": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", - "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.0", - "exponential-backoff": "^3.1.1", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^14.0.3", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", - "semver": "^7.3.5", - "tar": "^7.4.3", - "tinyglobby": "^0.2.12", - "which": "^5.0.0" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/node-gyp-build": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "license": "MIT", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, - "node_modules/node-gyp/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, - "node_modules/node-sqlite3-wasm": { - "version": "0.8.53", - "resolved": "https://registry.npmjs.org/node-sqlite3-wasm/-/node-sqlite3-wasm-0.8.53.tgz", - "integrity": "sha512-HPuGOPj3L+h3WSf0XikIXTDpsRxlVmzBC3RMgqi3yDg9CEbm/4Hw3rrDodeITqITjm07X4atWLlDMMI8KERMiQ==", - "license": "MIT" - }, - "node_modules/nopt": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", - "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", - "dev": true, - "license": "ISC", - "dependencies": { - "abbrev": "^3.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/orderedmap": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", "license": "MIT" }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", - "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6362,30 +3097,6 @@ "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/pdfkit": { "version": "0.17.2", "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.17.2.tgz", @@ -6399,28 +3110,6 @@ "png-js": "^1.0.0" } }, - "node_modules/pe-library": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/pe-library/-/pe-library-0.4.1.tgz", - "integrity": "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6439,21 +3128,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/plist": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", - "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xmldom/xmldom": "^0.8.8", - "base64-js": "^1.5.1", - "xmlbuilder": "^15.1.1" - }, - "engines": { - "node": ">=10.4.0" - } - }, "node_modules/png-js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", @@ -6493,88 +3167,12 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, - "node_modules/postject": { - "version": "1.0.0-alpha.6", - "resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz", - "integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "commander": "^9.4.0" - }, - "bin": { - "postject": "dist/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/postject/node_modules/commander": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", - "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": "^12.20.0 || >=14" - } - }, - "node_modules/proc-log": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", - "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, "node_modules/prosemirror-changeset": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", @@ -6774,29 +3372,9 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, "license": "MIT" }, - "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -6806,19 +3384,6 @@ "node": ">=6" } }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react": { "version": "19.2.4", "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", @@ -6905,34 +3470,6 @@ "react-dom": ">=18" } }, - "node_modules/read-binary-file-arch": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz", - "integrity": "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "bin": { - "read-binary-file-arch": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6943,108 +3480,12 @@ "node": ">=0.10.0" } }, - "node_modules/resedit": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", - "integrity": "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pe-library": "^0.4.1" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/jet2jet" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/restructure": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", "integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==", "license": "MIT" }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/roarr": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", - "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "dependencies": { - "boolean": "^3.0.1", - "detect-node": "^2.0.4", - "globalthis": "^1.0.1", - "json-stringify-safe": "^5.0.1", - "semver-compare": "^1.0.0", - "sprintf-js": "^1.1.2" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/rolldown": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", @@ -7100,44 +3541,6 @@ "tslib": "^2.1.0" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "license": "MIT" - }, - "node_modules/sanitize-filename": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.3.tgz", - "integrity": "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg==", - "dev": true, - "license": "WTFPL OR ISC", - "dependencies": { - "truncate-utf8-bytes": "^1.0.0" - } - }, "node_modules/sax": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", @@ -7153,41 +3556,6 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/serialize-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", - "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "type-fest": "^0.13.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/set-cookie-parser": { "version": "2.7.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", @@ -7236,106 +3604,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/simple-update-notifier/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slice-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", - "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -7345,58 +3613,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/ssri": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", - "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/stat-mode": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-1.0.0.tgz", - "integrity": "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -7412,22 +3628,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7441,33 +3641,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sumchecker": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", - "integrity": "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.1.0" - }, - "engines": { - "node": ">= 8.0" - } - }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -7503,129 +3676,12 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.1.0", - "yallist": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", - "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/temp": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", - "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mkdirp": "^0.5.1", - "rimraf": "~2.6.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/temp-file": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz", - "integrity": "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-exit-hook": "^2.0.1", - "fs-extra": "^10.0.0" - } - }, - "node_modules/temp-file/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/temp-file/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/temp-file/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/tiny-async-pool": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", - "integrity": "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^5.5.0" - } - }, - "node_modules/tiny-async-pool/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "license": "MIT" }, - "node_modules/tiny-typed-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz", - "integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==", - "license": "MIT" - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -7642,26 +3698,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.14" - } - }, - "node_modules/tmp-promise": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", - "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "tmp": "^0.2.0" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -7672,36 +3708,12 @@ "tree-kill": "cli.js" } }, - "node_modules/truncate-utf8-bytes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz", - "integrity": "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==", - "dev": true, - "license": "WTFPL", - "dependencies": { - "utf8-byte-length": "^1.0.1" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -7726,6 +3738,7 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, "license": "MIT" }, "node_modules/unicode-properties": { @@ -7754,42 +3767,6 @@ "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", "license": "MIT" }, - "node_modules/unique-filename": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", - "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/unique-slug": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", - "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -7820,16 +3797,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -7839,35 +3806,12 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/utf8-byte-length": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz", - "integrity": "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==", - "dev": true, - "license": "(WTFPL OR MIT)" - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/vite": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", @@ -8229,32 +4173,6 @@ "node": ">=20.0.0" } }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/which": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", - "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -8273,32 +4191,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, - "license": "ISC" - }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -8317,16 +4209,6 @@ "xml-js": "bin/cli.js" } }, - "node_modules/xmlbuilder": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", - "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.0" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -8337,13 +4219,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, - "license": "ISC" - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -8372,30 +4247,6 @@ "engines": { "node": ">=12" } - }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/package.json b/package.json index d851ea2..4344710 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,19 @@ "productName": "ERitors Scribe", "version": "0.5.0", "type": "module", - "main": "dist/electron/main.js", "scripts": { "dev": "vite --port 4000", "build": "vite build", "preview": "vite preview", "tauri:dev": "tauri dev", - "tauri:build": "tauri build" + "tauri:build": "dotenv -- tauri build", + "tauri:build:win": "PATH=\"/opt/homebrew/opt/llvm/bin:$PATH\" dotenv -- tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc" }, "keywords": [], "author": "", "license": "ISC", "description": "", "devDependencies": { - "@electron/notarize": "^3.1.1", "@tauri-apps/cli": "^2.10.1", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.10.1", @@ -25,14 +24,14 @@ "@types/react-dom": "^19.2.3", "concurrently": "^9.2.1", "dotenv": "^17.2.3", - "electron": "^39.2.1", - "electron-builder": "^26.0.12", + "dotenv-cli": "^11.0.0", "typescript": "^5.9.3", "wait-on": "^9.0.3" }, "dependencies": { "@tailwindcss/postcss": "^4.1.17", "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-http": "^2.5.8", "@tauri-apps/plugin-shell": "^2.3.5", "@tiptap/extension-color": "^3.10.7", "@tiptap/extension-gapcursor": "^3.10.7", @@ -41,17 +40,12 @@ "@tiptap/extension-underline": "^3.10.7", "@tiptap/react": "^3.10.7", "@tiptap/starter-kit": "^3.10.7", - "@types/bcrypt": "^6.0.0", "@vitejs/plugin-react": "^6.0.1", "autoprefixer": "^10.4.22", - "axios": "^1.13.2", - "bcrypt": "^6.0.0", "docx": "^9.5.3", - "electron-updater": "^6.7.3", "i18next": "^25.10.4", "jszip": "^3.10.1", "lucide-react": "^0.577.0", - "node-sqlite3-wasm": "^0.8.51", "pdfkit": "^0.17.2", "postcss": "^8.5.6", "react": "^19.2.0", @@ -60,67 +54,5 @@ "react-router-dom": "^7.13.1", "tailwindcss": "^4.1.17", "vite": "^8.0.1" - }, - "build": { - "appId": "com.eritors.scribe.desktop", - "productName": "ERitors Scribe", - "buildDependenciesFromSource": false, - "nodeGypRebuild": false, - "npmRebuild": false, - "files": [ - "dist/**/*", - "out/**/*", - "build/**/*", - "package.json" - ], - "asarUnpack": [ - "out/**/*" - ], - "directories": { - "output": "release" - }, - "mac": { - "icon": "build/icons/mac/icon.icns", - "target": [ - "dmg", - "zip" - ], - "category": "public.app-category.productivity", - "hardenedRuntime": true, - "gatekeeperAssess": false, - "entitlements": "build/entitlements.mac.plist", - "entitlementsInherit": "build/entitlements.mac.plist" - }, - "afterSign": "scripts/notarize.cjs", - "win": { - "icon": "build/icons/win/icon.ico", - "target": [ - { - "target": "nsis", - "arch": [ - "x64", - "ia32" - ] - } - ] - }, - "linux": { - "icon": "build/icons/png", - "target": [ - "AppImage", - "deb" - ], - "category": "Utility" - }, - "nsis": { - "oneClick": false, - "allowToChangeInstallationDirectory": true, - "createDesktopShortcut": true, - "createStartMenuShortcut": true - }, - "publish": { - "provider": "generic", - "url": "https://eritors.com/download/app/desktop" - } } } diff --git a/src-tauri/src/crypto/key_manager.rs b/src-tauri/src/crypto/key_manager.rs index 5eb858a..14380ab 100644 --- a/src-tauri/src/crypto/key_manager.rs +++ b/src-tauri/src/crypto/key_manager.rs @@ -1,243 +1,274 @@ -use std::collections::HashMap; -use std::fs; -use std::path::PathBuf; - -use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; -use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; -use rand::RngCore; -use serde::{Deserialize, Serialize}; - -use crate::error::{AppError, AppResult}; - -const SERVICE_NAME: &str = "com.eritors.scribe.desktop"; -const IV_LENGTH: usize = 16; - -type Aes256CbcEnc = cbc::Encryptor; -type Aes256CbcDec = cbc::Decryptor; - -// ===== Secure vault: encrypted JSON file (like Electron's safeStorage) ===== - -#[derive(Serialize, Deserialize, Default)] -struct SecureVault { - token: Option, - encryption_keys: HashMap, - last_user_id: Option, - pin_hashes: HashMap, - #[serde(default)] - pin_failed_attempts: i32, - #[serde(default)] - pin_locked_until: Option, -} - -fn vault_path() -> PathBuf { - dirs_next::data_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join(SERVICE_NAME) - .join("secure-config.json") -} - -const KEYRING_USER: &str = "vault-key"; - -/// Retrieves (or generates and stores) the vault encryption key via the OS keyring -/// (macOS Keychain, Windows DPAPI, Linux Secret Service). -/// Falls back to the old derivation method if the keyring is unavailable, -/// and attempts to migrate the key into the keyring for next time. -fn get_vault_key() -> [u8; 32] { - let entry = keyring::Entry::new(SERVICE_NAME, KEYRING_USER); - if let Ok(entry) = &entry { - if let Ok(stored) = entry.get_password() { - if let Ok(decoded) = BASE64.decode(stored.trim()) { - if decoded.len() == 32 { - let mut key = [0u8; 32]; - key.copy_from_slice(&decoded); - return key; - } - } - } - } - let mut key = [0u8; 32]; - rand::rng().fill_bytes(&mut key); - let encoded = BASE64.encode(key); - if let Ok(entry) = &entry { - let _ = entry.set_password(&encoded); - } - key -} - -fn encrypt_vault(data: &[u8], key: &[u8; 32]) -> AppResult> { - let mut iv = [0u8; IV_LENGTH]; - rand::rng().fill_bytes(&mut iv); - let block_size = 16; - let padding_len = block_size - (data.len() % block_size); - let mut buf = Vec::with_capacity(data.len() + padding_len); - buf.extend_from_slice(data); - buf.extend(std::iter::repeat(padding_len as u8).take(padding_len)); - let padded_len = buf.len(); - let cipher = Aes256CbcEnc::new(key.into(), &iv.into()); - cipher.encrypt_padded_mut::(&mut buf, padded_len) - .map_err(|e| AppError::Encryption(format!("Vault encrypt failed: {}", e)))?; - let mut result = Vec::with_capacity(IV_LENGTH + buf.len()); - result.extend_from_slice(&iv); - result.extend_from_slice(&buf); - Ok(result) -} - -fn decrypt_vault(data: &[u8], key: &[u8; 32]) -> AppResult> { - if data.len() < IV_LENGTH + 1 { - return Err(AppError::Encryption("Vault data too short".into())); - } - let iv: [u8; 16] = data[..IV_LENGTH].try_into() - .map_err(|_| AppError::Encryption("Invalid IV".into()))?; - let mut buf = data[IV_LENGTH..].to_vec(); - let cipher = Aes256CbcDec::new(key.into(), &iv.into()); - let decrypted = cipher.decrypt_padded_mut::(&mut buf) - .map_err(|e| AppError::Encryption(format!("Vault decrypt failed: {}", e)))?; - Ok(decrypted.to_vec()) -} - -fn read_vault() -> AppResult { - let path = vault_path(); - if !path.exists() { - return Ok(SecureVault::default()); - } - let content = fs::read_to_string(&path) - .map_err(|e| AppError::Keyring(format!("Failed to read vault: {}", e)))?; - - if cfg!(debug_assertions) { - return serde_json::from_str::(&content) - .map_err(|e| AppError::Keyring(format!("Vault JSON parse error: {}", e))); - } - - let raw = BASE64.decode(content.trim()) - .map_err(|e| AppError::Keyring(format!("Vault corrupted (base64): {}", e)))?; - - let key = get_vault_key(); - if let Ok(decrypted) = decrypt_vault(&raw, &key) { - if let Ok(vault) = serde_json::from_slice::(&decrypted) { - return Ok(vault); - } - } - - Err(AppError::Keyring("Vault decryption failed — cannot read existing vault".into())) -} - -fn write_vault(vault: &SecureVault) -> AppResult<()> { - let path = vault_path(); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create vault dir: {}", e)))?; - } - - if cfg!(debug_assertions) { - let json = serde_json::to_string_pretty(vault) - .map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?; - return fs::write(&path, json).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e))); - } - - let key = get_vault_key(); - let json = serde_json::to_string(vault) - .map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?; - let encrypted = encrypt_vault(json.as_bytes(), &key)?; - let encoded = BASE64.encode(&encrypted); - fs::write(&path, encoded).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e))) -} - -// ===== Public API (same signatures as before) ===== - -pub fn get_user_encryption_key(user_id: &str) -> AppResult { - let vault = read_vault()?; - vault.encryption_keys.get(user_id).cloned() - .ok_or_else(|| AppError::Keyring(format!("No encryption key for user {}", user_id))) -} - -pub fn set_user_encryption_key(user_id: &str, encryption_key: &str) -> AppResult<()> { - let mut vault = read_vault()?; - vault.encryption_keys.insert(user_id.to_string(), encryption_key.to_string()); - write_vault(&vault) -} - -pub fn has_user_encryption_key(user_id: &str) -> AppResult { - let vault = read_vault()?; - Ok(vault.encryption_keys.contains_key(user_id)) -} - - -pub fn get_token() -> AppResult> { - let vault = read_vault()?; - Ok(vault.token) -} - -pub fn set_token(token: &str) -> AppResult<()> { - let mut vault = read_vault()?; - vault.token = Some(token.to_string()); - write_vault(&vault) -} - -pub fn remove_token() -> AppResult<()> { - let mut vault = read_vault()?; - vault.token = None; - write_vault(&vault) -} - -pub fn set_pin_hash(user_id: &str, pin_hash: &str) -> AppResult<()> { - let mut vault = read_vault()?; - vault.pin_hashes.insert(user_id.to_string(), pin_hash.to_string()); - write_vault(&vault) -} - -pub fn get_pin_hash(user_id: &str) -> AppResult> { - let vault = read_vault()?; - Ok(vault.pin_hashes.get(user_id).cloned()) -} - -pub fn set_last_user_id(user_id: &str) -> AppResult<()> { - let mut vault = read_vault()?; - vault.last_user_id = Some(user_id.to_string()); - write_vault(&vault) -} - -pub fn get_last_user_id() -> AppResult> { - let vault = read_vault()?; - Ok(vault.last_user_id) -} - -pub fn clear_vault() -> AppResult<()> { - write_vault(&SecureVault::default()) -} - -const MAX_PIN_ATTEMPTS: i32 = 5; -const PIN_LOCKOUT_SECONDS: i64 = 300; // 5 minutes - -pub fn check_pin_rate_limit() -> AppResult<()> { - let vault = read_vault()?; - if let Some(locked_until) = vault.pin_locked_until { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs() as i64) - .unwrap_or(0); - if now < locked_until { - let remaining = locked_until - now; - return Err(AppError::Auth(format!("Too many attempts. Try again in {} seconds.", remaining))); - } - } - Ok(()) -} - -pub fn record_pin_failure() -> AppResult<()> { - let mut vault = read_vault()?; - vault.pin_failed_attempts += 1; - if vault.pin_failed_attempts >= MAX_PIN_ATTEMPTS { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs() as i64) - .unwrap_or(0); - vault.pin_locked_until = Some(now + PIN_LOCKOUT_SECONDS); - } - write_vault(&vault) -} - -pub fn reset_pin_attempts() -> AppResult<()> { - let mut vault = read_vault()?; - vault.pin_failed_attempts = 0; - vault.pin_locked_until = None; - write_vault(&vault) -} +use std::collections::HashMap; +use std::fs; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +use crate::error::{AppError, AppResult}; + +const SERVICE_NAME: &str = "com.eritors.scribe.desktop"; + +// ===== DEV: plain JSON vault (everything in one file, no encryption) ===== + +#[derive(Serialize, Deserialize, Default)] +struct SecureVault { + token: Option, + encryption_keys: HashMap, + last_user_id: Option, + pin_hashes: HashMap, + #[serde(default)] + pin_failed_attempts: i32, + #[serde(default)] + pin_locked_until: Option, +} + +// ===== PROD: plain JSON for non-sensitive config only ===== + +#[derive(Serialize, Deserialize, Default)] +struct AppConfig { + last_user_id: Option, + #[serde(default)] + pin_failed_attempts: i32, + #[serde(default)] + pin_locked_until: Option, +} + +fn config_path() -> PathBuf { + dirs_next::data_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(SERVICE_NAME) + .join(if cfg!(debug_assertions) { "secure-config.json" } else { "app-config.json" }) +} + +// ── DEV helpers ────────────────────────────────────────────────────────── + +fn read_vault() -> AppResult { + let path = config_path(); + if !path.exists() { return Ok(SecureVault::default()); } + let content = fs::read_to_string(&path) + .map_err(|e| AppError::Keyring(format!("Failed to read vault: {}", e)))?; + serde_json::from_str::(&content) + .map_err(|e| AppError::Keyring(format!("Vault JSON parse error: {}", e))) +} + +fn write_vault(vault: &SecureVault) -> AppResult<()> { + let path = config_path(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create dir: {}", e)))?; + } + let json = serde_json::to_string_pretty(vault) + .map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?; + fs::write(&path, json).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e))) +} + +// ── PROD helpers ───────────────────────────────────────────────────────── + +fn read_config() -> AppResult { + let path = config_path(); + if !path.exists() { return Ok(AppConfig::default()); } + let content = fs::read_to_string(&path) + .map_err(|e| AppError::Keyring(format!("Failed to read config: {}", e)))?; + serde_json::from_str::(&content) + .map_err(|e| AppError::Keyring(format!("Config JSON parse error: {}", e))) +} + +fn write_config(config: &AppConfig) -> AppResult<()> { + let path = config_path(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create dir: {}", e)))?; + } + let json = serde_json::to_string_pretty(config) + .map_err(|e| AppError::Internal(format!("Failed to serialize config: {}", e)))?; + fs::write(&path, json).map_err(|e| AppError::Internal(format!("Failed to write config: {}", e))) +} + +fn keyring_entry(account: &str) -> AppResult { + keyring::Entry::new(SERVICE_NAME, account) + .map_err(|e| AppError::Keyring(format!("Keyring entry error: {}", e))) +} + +fn keyring_get(account: &str) -> AppResult> { + let entry = keyring_entry(account)?; + match entry.get_password() { + Ok(val) => Ok(Some(val)), + Err(keyring::Error::NoEntry) => Ok(None), + Err(e) => Err(AppError::Keyring(format!("Keyring read error: {}", e))), + } +} + +fn keyring_set(account: &str, value: &str) -> AppResult<()> { + keyring_entry(account)?.set_password(value) + .map_err(|e| AppError::Keyring(format!("Keyring write error: {}", e))) +} + +fn keyring_delete(account: &str) -> AppResult<()> { + let entry = keyring_entry(account)?; + match entry.delete_credential() { + Ok(()) | Err(keyring::Error::NoEntry) => Ok(()), + Err(e) => Err(AppError::Keyring(format!("Keyring delete error: {}", e))), + } +} + +// ===== Public API (signatures unchanged) ===== + +pub fn get_user_encryption_key(user_id: &str) -> AppResult { + if cfg!(debug_assertions) { + let vault = read_vault()?; + return vault.encryption_keys.get(user_id).cloned() + .ok_or_else(|| AppError::Keyring(format!("No encryption key for user {}", user_id))); + } + keyring_get(&format!("encryption_key:{}", user_id))? + .ok_or_else(|| AppError::Keyring(format!("No encryption key for user {}", user_id))) +} + +pub fn set_user_encryption_key(user_id: &str, encryption_key: &str) -> AppResult<()> { + if cfg!(debug_assertions) { + let mut vault = read_vault()?; + vault.encryption_keys.insert(user_id.to_string(), encryption_key.to_string()); + return write_vault(&vault); + } + keyring_set(&format!("encryption_key:{}", user_id), encryption_key) +} + +pub fn has_user_encryption_key(user_id: &str) -> AppResult { + if cfg!(debug_assertions) { + let vault = read_vault()?; + return Ok(vault.encryption_keys.contains_key(user_id)); + } + Ok(keyring_get(&format!("encryption_key:{}", user_id))?.is_some()) +} + +pub fn get_token() -> AppResult> { + if cfg!(debug_assertions) { + let vault = read_vault()?; + return Ok(vault.token); + } + keyring_get("token") +} + +pub fn set_token(token: &str) -> AppResult<()> { + if cfg!(debug_assertions) { + let mut vault = read_vault()?; + vault.token = Some(token.to_string()); + return write_vault(&vault); + } + keyring_set("token", token) +} + +pub fn remove_token() -> AppResult<()> { + if cfg!(debug_assertions) { + let mut vault = read_vault()?; + vault.token = None; + return write_vault(&vault); + } + keyring_delete("token") +} + +pub fn set_pin_hash(user_id: &str, pin_hash: &str) -> AppResult<()> { + if cfg!(debug_assertions) { + let mut vault = read_vault()?; + vault.pin_hashes.insert(user_id.to_string(), pin_hash.to_string()); + return write_vault(&vault); + } + keyring_set(&format!("pin_hash:{}", user_id), pin_hash) +} + +pub fn get_pin_hash(user_id: &str) -> AppResult> { + if cfg!(debug_assertions) { + let vault = read_vault()?; + return Ok(vault.pin_hashes.get(user_id).cloned()); + } + keyring_get(&format!("pin_hash:{}", user_id)) +} + +pub fn set_last_user_id(user_id: &str) -> AppResult<()> { + if cfg!(debug_assertions) { + let mut vault = read_vault()?; + vault.last_user_id = Some(user_id.to_string()); + return write_vault(&vault); + } + let mut config = read_config()?; + config.last_user_id = Some(user_id.to_string()); + write_config(&config) +} + +pub fn get_last_user_id() -> AppResult> { + if cfg!(debug_assertions) { + let vault = read_vault()?; + return Ok(vault.last_user_id); + } + let config = read_config()?; + Ok(config.last_user_id) +} + +pub fn clear_vault() -> AppResult<()> { + if cfg!(debug_assertions) { + return write_vault(&SecureVault::default()); + } + // Prod: delete all known keyring entries + reset config + keyring_delete("token")?; + if let Ok(config) = read_config() { + if let Some(uid) = &config.last_user_id { + keyring_delete(&format!("encryption_key:{}", uid))?; + keyring_delete(&format!("pin_hash:{}", uid))?; + } + } + write_config(&AppConfig::default()) +} + +const MAX_PIN_ATTEMPTS: i32 = 5; +const PIN_LOCKOUT_SECONDS: i64 = 300; + +pub fn check_pin_rate_limit() -> AppResult<()> { + let (locked_until, _) = if cfg!(debug_assertions) { + let vault = read_vault()?; + (vault.pin_locked_until, vault.pin_failed_attempts) + } else { + let config = read_config()?; + (config.pin_locked_until, config.pin_failed_attempts) + }; + if let Some(locked_until) = locked_until { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0); + if now < locked_until { + return Err(AppError::Auth(format!("Too many attempts. Try again in {} seconds.", locked_until - now))); + } + } + Ok(()) +} + +pub fn record_pin_failure() -> AppResult<()> { + if cfg!(debug_assertions) { + let mut vault = read_vault()?; + vault.pin_failed_attempts += 1; + if vault.pin_failed_attempts >= MAX_PIN_ATTEMPTS { + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64).unwrap_or(0); + vault.pin_locked_until = Some(now + PIN_LOCKOUT_SECONDS); + } + return write_vault(&vault); + } + let mut config = read_config()?; + config.pin_failed_attempts += 1; + if config.pin_failed_attempts >= MAX_PIN_ATTEMPTS { + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs() as i64).unwrap_or(0); + config.pin_locked_until = Some(now + PIN_LOCKOUT_SECONDS); + } + write_config(&config) +} + +pub fn reset_pin_attempts() -> AppResult<()> { + if cfg!(debug_assertions) { + let mut vault = read_vault()?; + vault.pin_failed_attempts = 0; + vault.pin_locked_until = None; + return write_vault(&vault); + } + let mut config = read_config()?; + config.pin_failed_attempts = 0; + config.pin_locked_until = None; + write_config(&config) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index db4873a..651555a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -22,6 +22,7 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_http::init()) .manage(db_manager) .manage(session) .invoke_handler(tauri::generate_handler![ diff --git a/tsconfig.electron.json b/tsconfig.electron.json deleted file mode 100644 index f58d065..0000000 --- a/tsconfig.electron.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "compilerOptions": { - "module": "node16", - "moduleResolution": "node16", - "target": "ES2022", - "outDir": "dist/electron", - "rootDir": "electron", - "lib": ["ES2022"], - "esModuleInterop": true, - "skipLibCheck": true, - "strict": true, - "resolveJsonModule": true, - "noEmit": false - }, - "include": [ - "electron/**/*" - ], - "exclude": [ - "node_modules", - "dist", - "src", - ".next", - "out", - "lib", - "components", - "app", - "context", - "electron/preload.ts" - ] -} diff --git a/tsconfig.preload.json b/tsconfig.preload.json deleted file mode 100644 index 49f9906..0000000 --- a/tsconfig.preload.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "node", - "target": "ES2022", - "outDir": "dist/electron", - "lib": ["ES2022"], - "esModuleInterop": true, - "skipLibCheck": true, - "strict": true, - "resolveJsonModule": true, - "noEmit": false - }, - "include": [ - "electron/preload.ts" - ], - "exclude": [ - "node_modules" - ] -} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..e3c362d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,23 @@ +import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, '.'), + }, + }, + server: { + port: 4000, + strictPort: true, + }, + build: { + outDir: 'out', + emptyOutDir: true, + }, + css: { + postcss: './postcss.config.cjs', + }, +});