Remove Act, AutoUpdater, and Book IPC modules alongside associated database logic.
This commit is contained in:
@@ -58,7 +58,7 @@ export default function LoginForm() {
|
|||||||
await tauri.loginSuccess();
|
await tauri.loginSuccess();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(t('loginForm.error.server'));
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t('loginForm.error.unknown'));
|
errorMessage(t('loginForm.error.unknown'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default function LoginPage() {
|
|||||||
await tauri.loginSuccess();
|
await tauri.loginSuccess();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(t('loginForm.error.server'));
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t('loginForm.error.unknown'));
|
errorMessage(t('loginForm.error.unknown'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default function ForgetPasswordPage() {
|
|||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(t('resetPassword.error.emailServer'));
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t('resetPassword.error.emailUnknown'));
|
errorMessage(t('resetPassword.error.emailUnknown'));
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ export default function ForgetPasswordPage() {
|
|||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(t('resetPassword.error.codeServer'));
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t('resetPassword.error.codeUnknown'));
|
errorMessage(t('resetPassword.error.codeUnknown'));
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ export default function ForgetPasswordPage() {
|
|||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(t('resetPassword.error.passwordServer'));
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t('resetPassword.error.passwordUnknown'));
|
errorMessage(t('resetPassword.error.passwordUnknown'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {fetch} from "@tauri-apps/plugin-http";
|
||||||
import React, {ChangeEvent, useContext, useEffect, useRef, useState} from 'react';
|
import React, {ChangeEvent, useContext, useEffect, useRef, useState} from 'react';
|
||||||
import {
|
import {
|
||||||
BarChart2,
|
BarChart2,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {X} from 'lucide-react';
|
|||||||
import IconButton from "@/components/ui/IconButton";
|
import IconButton from "@/components/ui/IconButton";
|
||||||
import React, {ChangeEvent, forwardRef, useContext, useImperativeHandle, useState} from "react";
|
import React, {ChangeEvent, forwardRef, useContext, useImperativeHandle, useState} from "react";
|
||||||
import {apiDelete, apiPost} from "@/lib/api/client";
|
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 {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||||
import {BookContext, BookContextProps} from "@/context/BookContext";
|
import {BookContext, BookContextProps} from "@/context/BookContext";
|
||||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||||
@@ -56,22 +56,17 @@ function BasicInformationSetting(_props: object, ref: React.ForwardedRef<Setting
|
|||||||
formData.append('picture', file);
|
formData.append('picture', file);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query: AxiosResponse<ArrayBuffer> = await axios({
|
const query: Response = await fetch(
|
||||||
method: "POST",
|
configs.apiUrl + `book/cover?bookid=${bookId}&lang=${lang}&plateforme=desktop`,
|
||||||
url: configs.apiUrl + `book/cover?bookid=${bookId}`,
|
{
|
||||||
headers: {
|
method: "POST",
|
||||||
'Authorization': `Bearer ${userToken}`,
|
headers: {'Authorization': `Bearer ${userToken}`},
|
||||||
},
|
body: formData,
|
||||||
params: {
|
}
|
||||||
lang: lang,
|
);
|
||||||
plateforme: 'web',
|
|
||||||
},
|
const contentType: string = query.headers.get('content-type') || 'image/jpeg';
|
||||||
data: formData,
|
const blob: Blob = new Blob([await query.arrayBuffer()], {type: contentType});
|
||||||
responseType: 'arraybuffer'
|
|
||||||
});
|
|
||||||
|
|
||||||
const contentType: string = query.headers['content-type'] || 'image/jpeg';
|
|
||||||
const blob: Blob = new Blob([query.data], {type: contentType});
|
|
||||||
const reader: FileReader = new FileReader();
|
const reader: FileReader = new FileReader();
|
||||||
|
|
||||||
reader.onloadend = function (): void {
|
reader.onloadend = function (): void {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import {fetch} from "@tauri-apps/plugin-http";
|
||||||
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
|
||||||
import {Check} from 'lucide-react';
|
import {Check} from 'lucide-react';
|
||||||
import Button from '@/components/ui/Button';
|
import Button from '@/components/ui/Button';
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
|||||||
setNewIncidentTitle('');
|
setNewIncidentTitle('');
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(t('errorAddIncident'));
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t('errorUnknownAddIncident'));
|
errorMessage(t('errorUnknownAddIncident'));
|
||||||
}
|
}
|
||||||
@@ -192,7 +192,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
|||||||
setSelectedIncidentId('');
|
setSelectedIncidentId('');
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(t('errorAddPlotPoint'));
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t('errorUnknownAddPlotPoint'));
|
errorMessage(t('errorUnknownAddPlotPoint'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {fetch} from "@tauri-apps/plugin-http";
|
||||||
import React, {ChangeEvent, useContext, useEffect, useState} from "react";
|
import React, {ChangeEvent, useContext, useEffect, useState} from "react";
|
||||||
import {Editor, EditorContent, useEditor} from "@tiptap/react";
|
import {Editor, EditorContent, useEditor} from "@tiptap/react";
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi
|
|||||||
localStorage.setItem('userEditorSettings', JSON.stringify(settings));
|
localStorage.setItem('userEditorSettings', JSON.stringify(settings));
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(t('userEditorSettings.saveError'));
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t('userEditorSettings.unknownError'));
|
errorMessage(t('userEditorSettings.unknownError'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import {fetch} from "@tauri-apps/plugin-http";
|
||||||
import React, {ChangeEvent, useContext, useState} from 'react';
|
import React, {ChangeEvent, useContext, useState} from 'react';
|
||||||
import {BookOpen, FileInput, Hash, Palette, Wand2} from 'lucide-react';
|
import {BookOpen, FileInput, Hash, Palette, Wand2} from 'lucide-react';
|
||||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||||
@@ -76,7 +77,7 @@ export default function GhostWriter() {
|
|||||||
content = editor?.getText();
|
content = editor?.getText();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(t('ghostWriter.errorRetrieveContent'));
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t('ghostWriter.errorUnknownRetrieveContent'));
|
errorMessage(t('ghostWriter.errorUnknownRetrieveContent'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function ScribeFooterBar() {
|
|||||||
setParagraphCount(paragraphs);
|
setParagraphCount(paragraphs);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(t('errors.wordCountError') + ` (${e.message})`);
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t('errors.wordCountError'));
|
errorMessage(t('errors.wordCountError'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export default function ScribeChapterComponent() {
|
|||||||
});
|
});
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(t("scribeChapterComponent.errorChapterUpdateFr"));
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t("scribeChapterComponent.errorChapterUpdateEn"));
|
errorMessage(t("scribeChapterComponent.errorChapterUpdateEn"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function InspireMe({hasKey}: { hasKey: boolean }): React.JSX.Elem
|
|||||||
content = htmlToText(content);
|
content = htmlToText(content);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
errorMessage(t('inspireMe.error.contentRetrieval'));
|
errorMessage(e.message);
|
||||||
} else {
|
} else {
|
||||||
errorMessage(t('inspireMe.error.contentRetrievalUnknown'));
|
errorMessage(t('inspireMe.error.contentRetrievalUnknown'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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<string>('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<void, BookProps[]>(
|
|
||||||
* 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<string, BookProps>(
|
|
||||||
* 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<CreateBookData, string>(
|
|
||||||
* 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<string, void>(
|
|
||||||
* async (userId, bookId, lang) => {
|
|
||||||
* await Book.deleteBook(bookId, userId, lang);
|
|
||||||
* }
|
|
||||||
* )
|
|
||||||
* );
|
|
||||||
* // Frontend: invoke('db:book:delete', bookId)
|
|
||||||
*/
|
|
||||||
export function createHandler<TBody = void, TReturn = void>(
|
|
||||||
handler: (userId: string, body: TBody, lang: 'fr' | 'en') => TReturn | Promise<TReturn>
|
|
||||||
): (event: IpcMainInvokeEvent, body?: TBody) => Promise<TReturn> {
|
|
||||||
return async function(event: IpcMainInvokeEvent, body?: TBody): Promise<TReturn> {
|
|
||||||
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.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -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(/<br\s*\/?>/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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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<string>(`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}`);
|
|
||||||
}
|
|
||||||
@@ -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<ActProps[]> {
|
|
||||||
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<boolean> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<BookProps[]> {
|
|
||||||
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<BookProps> => {
|
|
||||||
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<string> {
|
|
||||||
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<BookProps> {
|
|
||||||
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 = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<string, unknown>;
|
|
||||||
marks?: TipTapMark[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TipTapMark {
|
|
||||||
type: string;
|
|
||||||
attrs?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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<number, ActStory> = {};
|
|
||||||
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, '"')
|
|
||||||
.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 = `<strong>${renderedText}</strong>`;
|
|
||||||
break;
|
|
||||||
case 'italic':
|
|
||||||
renderedText = `<em>${renderedText}</em>`;
|
|
||||||
break;
|
|
||||||
case 'underline':
|
|
||||||
renderedText = `<u>${renderedText}</u>`;
|
|
||||||
break;
|
|
||||||
case 'strike':
|
|
||||||
renderedText = `<s>${renderedText}</s>`;
|
|
||||||
break;
|
|
||||||
case 'code':
|
|
||||||
renderedText = `<code>${renderedText}</code>`;
|
|
||||||
break;
|
|
||||||
case 'link':
|
|
||||||
const linkHref: string = (mark.attrs?.href as string) || '#';
|
|
||||||
renderedText = `<a href="${escapeHtmlCharacters(linkHref)}">${renderedText}</a>`;
|
|
||||||
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 `<p${textAlignStyle}>${childrenHtml || '\u00A0'}</p>`;
|
|
||||||
case 'heading':
|
|
||||||
const headingLevel: number = (node.attrs?.level as number) || 1;
|
|
||||||
return `<h${headingLevel}${textAlignStyle}>${childrenHtml}</h${headingLevel}>`;
|
|
||||||
case 'bulletList':
|
|
||||||
return `<ul>${childrenHtml}</ul>`;
|
|
||||||
case 'orderedList':
|
|
||||||
return `<ol>${childrenHtml}</ol>`;
|
|
||||||
case 'listItem':
|
|
||||||
return `<li>${childrenHtml}</li>`;
|
|
||||||
case 'blockquote':
|
|
||||||
return `<blockquote>${childrenHtml}</blockquote>`;
|
|
||||||
case 'codeBlock':
|
|
||||||
return `<pre><code>${childrenHtml}</code></pre>`;
|
|
||||||
case 'hardBreak':
|
|
||||||
return '<br />';
|
|
||||||
case 'horizontalRule':
|
|
||||||
return '<hr />';
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<string, Attribute[]> = new Map<string, Attribute[]>();
|
|
||||||
|
|
||||||
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<string, CompleteCharacterProps>();
|
|
||||||
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<string, CompleteCharacterProps>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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(/<br\s*\/?>/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 += '<p>';
|
|
||||||
if (node.content) {
|
|
||||||
node.content.forEach((childNode: TiptapNode) => {
|
|
||||||
html += this.convertTiptapToHTML(childNode);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
html += '</p>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'text':
|
|
||||||
let formattedText = node.text || '';
|
|
||||||
|
|
||||||
if (node.attrs) {
|
|
||||||
if (node.attrs.bold) {
|
|
||||||
formattedText = `<strong>${formattedText}</strong>`;
|
|
||||||
}
|
|
||||||
if (node.attrs.italic) {
|
|
||||||
formattedText = `<em>${formattedText}</em>`;
|
|
||||||
}
|
|
||||||
if (node.attrs.underline) {
|
|
||||||
formattedText = `<u>${formattedText}</u>`;
|
|
||||||
}
|
|
||||||
if (node.attrs.strike) {
|
|
||||||
formattedText = `<s>${formattedText}</s>`;
|
|
||||||
}
|
|
||||||
if (node.attrs.link) {
|
|
||||||
formattedText = `<a href="${node.attrs.link.href}">${formattedText}</a>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
html += formattedText;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'heading':
|
|
||||||
const headingLevel = node.attrs?.level || 1;
|
|
||||||
html += `<h${headingLevel}>`;
|
|
||||||
if (node.content) {
|
|
||||||
node.content.forEach((childNode: TiptapNode) => {
|
|
||||||
html += this.convertTiptapToHTML(childNode);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
html += `</h${headingLevel}>`;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'bulletList':
|
|
||||||
html += '<ul>';
|
|
||||||
if (node.content) {
|
|
||||||
node.content.forEach((childNode: TiptapNode) => {
|
|
||||||
html += this.convertTiptapToHTML(childNode);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
html += '</ul>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'orderedList':
|
|
||||||
html += '<ol>';
|
|
||||||
if (node.content) {
|
|
||||||
node.content.forEach((childNode: TiptapNode) => {
|
|
||||||
html += this.convertTiptapToHTML(childNode);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
html += '</ol>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'listItem':
|
|
||||||
html += '<li>';
|
|
||||||
if (node.content) {
|
|
||||||
node.content.forEach((childNode: TiptapNode) => {
|
|
||||||
html += this.convertTiptapToHTML(childNode);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
html += '</li>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'blockquote':
|
|
||||||
html += '<blockquote>';
|
|
||||||
if (node.content) {
|
|
||||||
node.content.forEach((childNode: TiptapNode) => {
|
|
||||||
html += this.convertTiptapToHTML(childNode);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
html += '</blockquote>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'codeBlock':
|
|
||||||
html += '<pre><code>';
|
|
||||||
if (node.content) {
|
|
||||||
node.content.forEach((childNode: TiptapNode) => {
|
|
||||||
html += this.convertTiptapToHTML(childNode);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
html += '</code></pre>';
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.warn(`Unhandled node type: ${node.type}`);
|
|
||||||
if (node.content) {
|
|
||||||
node.content.forEach((childNode: TiptapNode) => {
|
|
||||||
html += this.convertTiptapToHTML(childNode);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<string> {
|
|
||||||
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<boolean> {
|
|
||||||
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 '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<boolean> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}`
|
|
||||||
@@ -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<ExportResult> {
|
|
||||||
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<ExportResult> {
|
|
||||||
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<void>((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<ExportResult> {
|
|
||||||
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', `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
|
|
||||||
<rootfiles>
|
|
||||||
<rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
|
|
||||||
</rootfiles>
|
|
||||||
</container>`);
|
|
||||||
|
|
||||||
let contentOpf: string = `<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<package xmlns="http://www.idpf.org/2007/opf" version="2.0" unique-identifier="ERitors-${bookId}">
|
|
||||||
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
||||||
<dc:title>${bookTitle}${bookData.subTitle ? ' - ' + bookData.subTitle : ''}</dc:title>
|
|
||||||
<dc:language>fr</dc:language>
|
|
||||||
<dc:identifier id="ERitors-${bookId}">urn:uuid:${bookId}</dc:identifier>
|
|
||||||
<dc:creator>${bookData.userInfos.firstName} ${bookData.userInfos.lastName}</dc:creator>
|
|
||||||
<dc:publisher>ERitors Scribe</dc:publisher>
|
|
||||||
<meta name="cover" content="cover-image-id" />
|
|
||||||
</metadata>
|
|
||||||
<manifest>`;
|
|
||||||
|
|
||||||
let spine: string = `<spine toc="toc">`;
|
|
||||||
|
|
||||||
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 = `<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
||||||
<head>
|
|
||||||
<title>${chapter.title}</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="styles.css"/>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${htmlContent}
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
|
|
||||||
epub.file(`OEBPS/${chapterIndex}.xhtml`, xhtmlPage);
|
|
||||||
contentOpf += `<item id="${chapterIndex}" href="${chapterIndex}.xhtml" media-type="application/xhtml+xml"/>`;
|
|
||||||
spine += `<itemref idref="${chapterIndex}" linear="yes"/>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
spine += `</spine>`;
|
|
||||||
|
|
||||||
contentOpf += `<item id="toc" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
|
|
||||||
<item id="style" href="styles.css" media-type="text/css"/>`;
|
|
||||||
contentOpf += spine;
|
|
||||||
contentOpf += `</package>`;
|
|
||||||
|
|
||||||
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`};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<GuideLineProps | null> {
|
|
||||||
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<boolean> {
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<IncidentProps[]> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<IssueProps[]> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<string, number>();
|
|
||||||
tagRecords.forEach((record: LocationElementQueryResult): void => {
|
|
||||||
elementCounts.set(record.element_id, (elementCounts.get(record.element_id) || 0) + 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
const subElements: SubElement[] = [];
|
|
||||||
const processedIds = new Set<string>();
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -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<PlotPointProps[]> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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<number, keyof SeriesWorldListProps> = {
|
|
||||||
0: 'laws',
|
|
||||||
1: 'biomes',
|
|
||||||
2: 'issues',
|
|
||||||
3: 'customs',
|
|
||||||
4: 'kingdoms',
|
|
||||||
5: 'climate',
|
|
||||||
6: 'resources',
|
|
||||||
7: 'wildlife',
|
|
||||||
8: 'arts',
|
|
||||||
9: 'ethnicGroups',
|
|
||||||
10: 'socialClasses',
|
|
||||||
11: 'importantCharacters'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class SeriesWorld {
|
|
||||||
/**
|
|
||||||
* Retrieves all worlds and their elements for a series.
|
|
||||||
* @param userId - The unique identifier of the user
|
|
||||||
* @param seriesId - The unique identifier of the series
|
|
||||||
* @param lang - The language for error messages ('fr' or 'en')
|
|
||||||
* @returns The list of worlds
|
|
||||||
*/
|
|
||||||
public static getWorldList(userId: string, seriesId: string, lang: 'fr' | 'en' = 'fr'): SeriesWorldListProps[] {
|
|
||||||
const userKey: string = getUserEncryptionKey(userId);
|
|
||||||
const worldsResult: SeriesWorldResult[] = SeriesWorldRepo.fetchWorlds(userId, seriesId, lang);
|
|
||||||
|
|
||||||
const worldsMap: Map<string, SeriesWorldListProps> = new Map();
|
|
||||||
|
|
||||||
for (const row of worldsResult) {
|
|
||||||
if (!worldsMap.has(row.world_id)) {
|
|
||||||
worldsMap.set(row.world_id, {
|
|
||||||
id: row.world_id,
|
|
||||||
name: row.world_name ? System.decryptDataWithUserKey(row.world_name, userKey) : '',
|
|
||||||
history: row.history ? System.decryptDataWithUserKey(row.history, userKey) : '',
|
|
||||||
politics: row.politics ? System.decryptDataWithUserKey(row.politics, userKey) : '',
|
|
||||||
economy: row.economy ? System.decryptDataWithUserKey(row.economy, userKey) : '',
|
|
||||||
religion: row.religion ? System.decryptDataWithUserKey(row.religion, userKey) : '',
|
|
||||||
languages: row.languages ? System.decryptDataWithUserKey(row.languages, userKey) : '',
|
|
||||||
laws: [],
|
|
||||||
biomes: [],
|
|
||||||
issues: [],
|
|
||||||
customs: [],
|
|
||||||
kingdoms: [],
|
|
||||||
climate: [],
|
|
||||||
resources: [],
|
|
||||||
wildlife: [],
|
|
||||||
arts: [],
|
|
||||||
ethnicGroups: [],
|
|
||||||
socialClasses: [],
|
|
||||||
importantCharacters: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.element_id) {
|
|
||||||
const world = worldsMap.get(row.world_id)!;
|
|
||||||
const element: SeriesWorldElementProps = {
|
|
||||||
id: row.element_id,
|
|
||||||
name: row.element_name ? System.decryptDataWithUserKey(row.element_name, userKey) : '',
|
|
||||||
description: row.element_description ? System.decryptDataWithUserKey(row.element_description, userKey) : ''
|
|
||||||
};
|
|
||||||
|
|
||||||
const key = ELEMENT_TYPE_MAP[row.element_type];
|
|
||||||
if (key && Array.isArray(world[key])) {
|
|
||||||
(world[key] as SeriesWorldElementProps[]).push(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(worldsMap.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new world to a series.
|
|
||||||
* @param userId - The unique identifier of the user
|
|
||||||
* @param seriesId - The unique identifier of the series
|
|
||||||
* @param name - The name of the world
|
|
||||||
* @param lang - The language for error messages ('fr' or 'en')
|
|
||||||
* @returns The new world ID
|
|
||||||
*/
|
|
||||||
public static addWorld(userId: string, seriesId: string, name: string, lang: 'fr' | 'en' = 'fr'): string {
|
|
||||||
const hashedName: string = System.hashElement(name);
|
|
||||||
|
|
||||||
const exists: boolean = SeriesWorldRepo.checkWorldExist(userId, seriesId, hashedName, lang);
|
|
||||||
if (exists) {
|
|
||||||
throw new Error(lang === 'fr' ? 'Un monde avec ce nom existe déjà.' : 'A world with this name already exists.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const userKey: string = getUserEncryptionKey(userId);
|
|
||||||
const worldId: string = System.createUniqueId();
|
|
||||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
|
||||||
|
|
||||||
SeriesWorldRepo.insertNewWorld(worldId, userId, seriesId, encryptedName, hashedName, lang);
|
|
||||||
return worldId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a world's information.
|
|
||||||
* @param userId - The unique identifier of the user
|
|
||||||
* @param worldId - The unique identifier of the world
|
|
||||||
* @param world - The updated world data
|
|
||||||
* @param lang - The language for error messages ('fr' or 'en')
|
|
||||||
* @returns True if successful
|
|
||||||
*/
|
|
||||||
public static updateWorld(userId: string, worldId: string, world: SeriesWorldUpdateProps, lang: 'fr' | 'en' = 'fr'): boolean {
|
|
||||||
const userKey: string = getUserEncryptionKey(userId);
|
|
||||||
const encryptedName: string = System.encryptDataWithUserKey(world.name, userKey);
|
|
||||||
const hashedName: string = System.hashElement(world.name);
|
|
||||||
const encryptedHistory: string | null = world.history ? System.encryptDataWithUserKey(world.history, userKey) : null;
|
|
||||||
const encryptedPolitics: string | null = world.politics ? System.encryptDataWithUserKey(world.politics, userKey) : null;
|
|
||||||
const encryptedEconomy: string | null = world.economy ? System.encryptDataWithUserKey(world.economy, userKey) : null;
|
|
||||||
const encryptedReligion: string | null = world.religion ? System.encryptDataWithUserKey(world.religion, userKey) : null;
|
|
||||||
const encryptedLanguages: string | null = world.languages ? System.encryptDataWithUserKey(world.languages, userKey) : null;
|
|
||||||
|
|
||||||
return SeriesWorldRepo.updateWorld(userId, worldId, encryptedName, hashedName, encryptedHistory, encryptedPolitics, encryptedEconomy, encryptedReligion, encryptedLanguages, lang);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new element to a world.
|
|
||||||
* @param userId - The unique identifier of the user
|
|
||||||
* @param worldId - The unique identifier of the world
|
|
||||||
* @param elementType - The type of element (0-11)
|
|
||||||
* @param name - The name of the element
|
|
||||||
* @param lang - The language for error messages ('fr' or 'en')
|
|
||||||
* @param description - The description of the element (optional)
|
|
||||||
* @returns The new element ID
|
|
||||||
*/
|
|
||||||
public static addElement(userId: string, worldId: string, elementType: number, name: string, lang: 'fr' | 'en' = 'fr', description?: string): string {
|
|
||||||
const userKey: string = getUserEncryptionKey(userId);
|
|
||||||
const elementId: string = System.createUniqueId();
|
|
||||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
|
||||||
const originalName: string = System.hashElement(name);
|
|
||||||
const encryptedDescription: string | null = description ? System.encryptDataWithUserKey(description, userKey) : null;
|
|
||||||
|
|
||||||
SeriesWorldRepo.insertElement(elementId, worldId, userId, elementType, encryptedName, originalName, encryptedDescription, lang);
|
|
||||||
return elementId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an element from a world.
|
|
||||||
* @param userId - The unique identifier of the user
|
|
||||||
* @param elementId - The unique identifier of the element
|
|
||||||
* @param 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<string, SpellTagProps> = new Map();
|
|
||||||
for (const tag of tags) {
|
|
||||||
tagMap.set(tag.id, tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
const spellResults: SpellResult[] = SpellRepo.fetchSpells(userId, bookId, lang);
|
|
||||||
|
|
||||||
const spells: SpellListItem[] = spellResults.map((spell: SpellResult): SpellListItem => {
|
|
||||||
const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey);
|
|
||||||
const decryptedDescription: string | 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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<CompleteBook> {
|
|
||||||
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<BookChapterContentTable[]> =>
|
|
||||||
ChapterContentRepository.fetchBookChapterContents(userId, chapter.chapter_id, lang))),
|
|
||||||
Promise.all(encryptedChapters.map((chapter: BookChaptersTable): Promise<BookChapterInfosTable[]> =>
|
|
||||||
ChapterRepo.fetchBookChapterInfos(userId, chapter.chapter_id, lang))),
|
|
||||||
Promise.all(encryptedCharacters.map((character: BookCharactersTable): Promise<BookCharactersAttributesTable[]> =>
|
|
||||||
CharacterRepo.fetchBookCharactersAttributes(userId, character.character_id, lang))),
|
|
||||||
Promise.all(encryptedWorlds.map((world: BookWorldTable): Promise<BookWorldElementsTable[]> =>
|
|
||||||
WorldRepository.fetchBookWorldElements(userId, world.world_id, lang))),
|
|
||||||
Promise.all(encryptedLocations.map((location: BookLocationTable): Promise<LocationElementTable[]> =>
|
|
||||||
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<LocationSubElementTable[]> =>
|
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<void> {
|
|
||||||
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<UserInfoResponse> {
|
|
||||||
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<string> {
|
|
||||||
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<boolean> {
|
|
||||||
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<UserAccount> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<string, number> = {
|
|
||||||
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<number, keyof WorldProps> = {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
import {Database, QueryResult, RunResult, SQLiteValue} from "node-sqlite3-wasm";
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface BookActSummariesTable extends Record<string, SQLiteValue> {
|
|
||||||
act_sum_id: string;
|
|
||||||
book_id: string;
|
|
||||||
user_id: string;
|
|
||||||
act_index: number;
|
|
||||||
last_update: number;
|
|
||||||
summary: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedActSummaryResult extends Record<string, SQLiteValue> {
|
|
||||||
act_sum_id: string;
|
|
||||||
book_id: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActQuery extends Record<string, SQLiteValue> {
|
|
||||||
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<BookActSummariesTable[]> {
|
|
||||||
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<BookActSummariesTable[]> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,495 +0,0 @@
|
|||||||
import {Database, QueryResult, RunResult, SQLiteValue} from 'node-sqlite3-wasm';
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface BookQuery extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
book_id: string;
|
|
||||||
type: string;
|
|
||||||
title: string;
|
|
||||||
sub_title: string | null;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BookCoverQuery extends Record<string, SQLiteValue> {
|
|
||||||
cover_image: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BookToolsTable extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<EritBooksTable[]> {
|
|
||||||
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<EritBooksTable[]> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,755 +0,0 @@
|
|||||||
import {Database, QueryResult, RunResult, SQLiteValue} from 'node-sqlite3-wasm';
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface ChapterQueryResult extends Record<string, SQLiteValue> {
|
|
||||||
chapter_id: string;
|
|
||||||
title: string;
|
|
||||||
chapter_order: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActChapterQuery extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
chapter_id: string;
|
|
||||||
version: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BookChaptersTable extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
chapter_id: string;
|
|
||||||
book_id: string;
|
|
||||||
title: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedChapterInfoResult extends Record<string, SQLiteValue> {
|
|
||||||
chapter_info_id: string;
|
|
||||||
chapter_id: string | null;
|
|
||||||
book_id: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChapterBookResult extends Record<string, SQLiteValue> {
|
|
||||||
title: string;
|
|
||||||
chapter_order: number;
|
|
||||||
content: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChapterExportInfoResult extends Record<string, SQLiteValue> {
|
|
||||||
chapter_id: string;
|
|
||||||
title: string;
|
|
||||||
chapter_order: number;
|
|
||||||
available_versions: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SelectedChapterContentResult extends Record<string, SQLiteValue> {
|
|
||||||
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<BookChaptersTable[]> {
|
|
||||||
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<BookChapterInfosTable[]> {
|
|
||||||
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<BookChaptersTable[]> {
|
|
||||||
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<BookChapterInfosTable[]> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,371 +0,0 @@
|
|||||||
import {Database, QueryResult, RunResult, SQLiteValue} from "node-sqlite3-wasm";
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface ChapterContentQueryResult extends Record<string, SQLiteValue> {
|
|
||||||
chapter_id: string;
|
|
||||||
version: number;
|
|
||||||
content: string;
|
|
||||||
words_count: number;
|
|
||||||
title: string;
|
|
||||||
chapter_order: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContentQueryResult extends Record<string, SQLiteValue> {
|
|
||||||
content: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CompanionContentQueryResult extends Record<string, SQLiteValue> {
|
|
||||||
version: number;
|
|
||||||
content: string;
|
|
||||||
words_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BookChapterContentTable extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<BookChapterContentTable[]> {
|
|
||||||
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<BookChapterContentTable[]> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,693 +0,0 @@
|
|||||||
import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface BookCharactersTable extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
character_id: string;
|
|
||||||
book_id: string;
|
|
||||||
first_name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedCharacterAttributeResult extends Record<string, SQLiteValue> {
|
|
||||||
attr_id: string;
|
|
||||||
character_id: string;
|
|
||||||
attribute_name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BookCharactersAttributesTable extends Record<string, SQLiteValue> {
|
|
||||||
attr_id: string;
|
|
||||||
character_id: string;
|
|
||||||
user_id: string;
|
|
||||||
attribute_name: string;
|
|
||||||
attribute_value: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CharacterResult extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
attr_id: string;
|
|
||||||
attribute_name: string;
|
|
||||||
attribute_value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CompleteCharacterResult extends Record<string, SQLiteValue> {
|
|
||||||
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<BookCharactersTable[]> {
|
|
||||||
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<BookCharactersAttributesTable[]> {
|
|
||||||
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<BookCharactersTable[]> {
|
|
||||||
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<BookCharactersAttributesTable[]> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,567 +0,0 @@
|
|||||||
import { Database, RunResult, SQLiteValue } from "node-sqlite3-wasm";
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface BookAIGuideLineTable extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
book_id: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedAIGuideLineResult extends Record<string, SQLiteValue> {
|
|
||||||
book_id: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GuideLineQuery extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<BookAIGuideLineTable[]> {
|
|
||||||
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<BookGuideLineTable[]> {
|
|
||||||
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<string, SQLiteValue> | undefined = db.get(query, params) as Record<string, SQLiteValue> | 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<string, SQLiteValue> | undefined = db.get(query, params) as Record<string, SQLiteValue> | 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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
import { Database, QueryResult, RunResult, SQLiteValue } from "node-sqlite3-wasm";
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface BookIncidentsTable extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
incident_id: string;
|
|
||||||
book_id: string;
|
|
||||||
title: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IncidentQuery extends Record<string, SQLiteValue> {
|
|
||||||
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<BookIncidentsTable[]> {
|
|
||||||
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<BookIncidentsTable[]> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
import { Database, QueryResult, RunResult, SQLiteValue } from "node-sqlite3-wasm";
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface BookIssuesTable extends Record<string, SQLiteValue> {
|
|
||||||
issue_id: string;
|
|
||||||
author_id: string;
|
|
||||||
book_id: string;
|
|
||||||
name: string;
|
|
||||||
hashed_issue_name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedIssueResult extends Record<string, SQLiteValue> {
|
|
||||||
issue_id: string;
|
|
||||||
book_id: string;
|
|
||||||
name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IssueQuery extends Record<string, SQLiteValue> {
|
|
||||||
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<BookIssuesTable[]> {
|
|
||||||
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<BookIssuesTable[]> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,900 +0,0 @@
|
|||||||
import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface LocationQueryResult extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
element_name: string;
|
|
||||||
element_description: string;
|
|
||||||
sub_elem_name: string;
|
|
||||||
sub_elem_description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BookLocationTable extends Record<string, SQLiteValue> {
|
|
||||||
loc_id: string;
|
|
||||||
book_id: string;
|
|
||||||
user_id: string;
|
|
||||||
loc_name: string;
|
|
||||||
loc_original_name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LocationElementTable extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
loc_id: string;
|
|
||||||
book_id: string;
|
|
||||||
loc_name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedLocationElementResult extends Record<string, SQLiteValue> {
|
|
||||||
element_id: string;
|
|
||||||
location: string;
|
|
||||||
element_name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedLocationSubElementResult extends Record<string, SQLiteValue> {
|
|
||||||
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<BookLocationTable[]> {
|
|
||||||
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<LocationElementTable[]> {
|
|
||||||
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<LocationSubElementTable[]> {
|
|
||||||
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<BookLocationTable[]> {
|
|
||||||
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<LocationElementTable[]> {
|
|
||||||
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<LocationSubElementTable[]> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
import { Database, QueryResult, RunResult, SQLiteValue } from "node-sqlite3-wasm";
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface BookPlotPointsTable extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
plot_point_id: string;
|
|
||||||
book_id: string;
|
|
||||||
title: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlotPointQuery extends Record<string, SQLiteValue> {
|
|
||||||
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<BookPlotPointsTable[]> {
|
|
||||||
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<BookPlotPointsTable[]> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
|
||||||
import System from '../System.js';
|
|
||||||
|
|
||||||
export interface RemovedItemRecord extends Record<string, SQLiteValue> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,541 +0,0 @@
|
|||||||
import {Database, QueryResult, RunResult, SQLiteValue} from 'node-sqlite3-wasm';
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface SeriesCharacterResult extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
attr_id: string;
|
|
||||||
attribute_name: string;
|
|
||||||
attribute_value: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SeriesCharactersTableResult extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
attr_id: string;
|
|
||||||
character_id: string;
|
|
||||||
user_id: string;
|
|
||||||
attribute_name: string;
|
|
||||||
attribute_value: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedSeriesCharacterResult extends Record<string, SQLiteValue> {
|
|
||||||
character_id: string;
|
|
||||||
series_id: string;
|
|
||||||
first_name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedSeriesCharacterAttributeResult extends Record<string, SQLiteValue> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,813 +0,0 @@
|
|||||||
import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface SeriesLocationResult extends Record<string, SQLiteValue> {
|
|
||||||
loc_id: string;
|
|
||||||
loc_name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SeriesLocationElementResult extends Record<string, SQLiteValue> {
|
|
||||||
element_id: string;
|
|
||||||
location_id: string;
|
|
||||||
element_name: string;
|
|
||||||
element_description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SeriesLocationSubElementResult extends Record<string, SQLiteValue> {
|
|
||||||
sub_element_id: string;
|
|
||||||
element_id: string;
|
|
||||||
sub_elem_name: string;
|
|
||||||
sub_elem_description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SeriesLocationsTableResult extends Record<string, SQLiteValue> {
|
|
||||||
loc_id: string;
|
|
||||||
series_id: string;
|
|
||||||
user_id: string;
|
|
||||||
loc_name: string;
|
|
||||||
loc_original_name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SeriesLocationElementsTableResult extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
loc_id: string;
|
|
||||||
series_id: string;
|
|
||||||
loc_name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedSeriesLocationElementResult extends Record<string, SQLiteValue> {
|
|
||||||
element_id: string;
|
|
||||||
location_id: string;
|
|
||||||
element_name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedSeriesLocationSubElementResult extends Record<string, SQLiteValue> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,647 +0,0 @@
|
|||||||
import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface SeriesSpellResult extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
tag_id: string;
|
|
||||||
name: string;
|
|
||||||
color: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SeriesSpellsTableResult extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
spell_id: string;
|
|
||||||
series_id: string;
|
|
||||||
name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedSeriesSpellTagResult extends Record<string, SQLiteValue> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<string, SQLiteValue> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,555 +0,0 @@
|
|||||||
import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface SeriesWorldResult extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
world_id: string;
|
|
||||||
series_id: string;
|
|
||||||
name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedSeriesWorldElementResult extends Record<string, SQLiteValue> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,553 +0,0 @@
|
|||||||
import { Database, QueryResult, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface SeriesResult extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
series_id: string;
|
|
||||||
book_id: string;
|
|
||||||
book_order: number;
|
|
||||||
title: string;
|
|
||||||
cover_image: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SeriesListItem extends Record<string, SQLiteValue> {
|
|
||||||
series_id: string;
|
|
||||||
name: string;
|
|
||||||
description: string | null;
|
|
||||||
cover_image: string | null;
|
|
||||||
book_count: number;
|
|
||||||
book_ids: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SeriesTableResult extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
series_id: string;
|
|
||||||
book_id: string;
|
|
||||||
book_order: number;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedSeriesResult extends Record<string, SQLiteValue> {
|
|
||||||
series_id: string;
|
|
||||||
name: string;
|
|
||||||
description: string | null;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedSeriesBookResult extends Record<string, SQLiteValue> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
|
||||||
import System from '../System.js';
|
|
||||||
|
|
||||||
export interface SpellResult extends Record<string, SQLiteValue> {
|
|
||||||
spell_id: string;
|
|
||||||
book_id: string;
|
|
||||||
name: string;
|
|
||||||
description: string | 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<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
spell_id: string;
|
|
||||||
book_id: string;
|
|
||||||
name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class SpellRepo {
|
|
||||||
/**
|
|
||||||
* Fetches all spells for a specific book owned by the user.
|
|
||||||
* @param userId - The unique identifier of the user
|
|
||||||
* @param bookId - The unique identifier of the book
|
|
||||||
* @param lang - The language for error messages ('fr' or 'en')
|
|
||||||
* @returns An array of spell results
|
|
||||||
*/
|
|
||||||
static fetchSpells(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellResult[] {
|
|
||||||
try {
|
|
||||||
const db: Database = System.getDb();
|
|
||||||
const query: string = 'SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes, 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.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,293 +0,0 @@
|
|||||||
import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
|
||||||
import System from '../System.js';
|
|
||||||
|
|
||||||
export interface SpellTagResult extends Record<string, SQLiteValue> {
|
|
||||||
tag_id: string;
|
|
||||||
book_id: string;
|
|
||||||
name: string;
|
|
||||||
color: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BookSpellTagsTable extends Record<string, SQLiteValue> {
|
|
||||||
tag_id: string;
|
|
||||||
book_id: string;
|
|
||||||
user_id: string;
|
|
||||||
name: string;
|
|
||||||
name_hash: string;
|
|
||||||
color: string | null;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedSpellTagResult extends Record<string, SQLiteValue> {
|
|
||||||
tag_id: string;
|
|
||||||
book_id: string;
|
|
||||||
name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class SpellTagRepo {
|
|
||||||
/**
|
|
||||||
* Fetches all spell tags for a specific book owned by the user.
|
|
||||||
* @param userId - The unique identifier of the user
|
|
||||||
* @param bookId - The unique identifier of the book
|
|
||||||
* @param lang - The language for error messages ('fr' or 'en')
|
|
||||||
* @returns An array of spell tag results
|
|
||||||
*/
|
|
||||||
static fetchSpellTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellTagResult[] {
|
|
||||||
try {
|
|
||||||
const db: Database = System.getDb();
|
|
||||||
const query: string = 'SELECT tag_id, book_id, name, color FROM book_spell_tags WHERE user_id=? AND book_id=?';
|
|
||||||
const params: SQLiteValue[] = [userId, bookId];
|
|
||||||
return db.all(query, params) as SpellTagResult[];
|
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
|
||||||
throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sorts.` : `Unable to retrieve spell tags.`);
|
|
||||||
} else {
|
|
||||||
console.error('[SpellTagRepo] An unknown error occurred.');
|
|
||||||
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts a new spell tag.
|
|
||||||
* @param tagId - The unique identifier for the new tag
|
|
||||||
* @param bookId - The unique identifier of the book
|
|
||||||
* @param userId - The unique identifier of the user
|
|
||||||
* @param name - The encrypted name of the tag
|
|
||||||
* @param nameHash - The hashed name for duplicate detection
|
|
||||||
* @param color - The optional color hex code
|
|
||||||
* @param lang - The language for error messages ('fr' or 'en')
|
|
||||||
* @returns The tag ID if successful
|
|
||||||
*/
|
|
||||||
static insertSpellTag(tagId: string, bookId: string, userId: string, name: string, nameHash: string, color: string | null, lang: 'fr' | 'en' = 'fr'): string {
|
|
||||||
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.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface UserInfosQueryResponse extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
first_name: string;
|
|
||||||
last_name: string;
|
|
||||||
username: string;
|
|
||||||
author_name: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GuideTourResult extends Record<string, SQLiteValue> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,583 +0,0 @@
|
|||||||
import { Database, QueryResult, RunResult, SQLiteValue } from "node-sqlite3-wasm";
|
|
||||||
import System from "../System.js";
|
|
||||||
|
|
||||||
export interface BookWorldTable extends Record<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
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<string, SQLiteValue> {
|
|
||||||
world_id: string;
|
|
||||||
book_id: string;
|
|
||||||
name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SyncedWorldElementResult extends Record<string, SQLiteValue> {
|
|
||||||
element_id: string;
|
|
||||||
world_id: string;
|
|
||||||
name: string;
|
|
||||||
last_update: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorldQuery extends Record<string, SQLiteValue> {
|
|
||||||
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<BookWorldTable[]> {
|
|
||||||
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<BookWorldElementsTable[]> {
|
|
||||||
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<BookWorldTable[]> {
|
|
||||||
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<BookWorldElementsTable[]> {
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -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<void, BookProps[]>(
|
|
||||||
async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise<BookProps[]> {
|
|
||||||
return await Book.getBooks(userId, lang);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// GET /books/synced - Get all synced books
|
|
||||||
ipcMain.handle('db:books:synced', createHandler<void, SyncedBook[]>(
|
|
||||||
async function(userId: string, _body: void, lang: 'fr' | 'en'):Promise<SyncedBook[]> {
|
|
||||||
return await Sync.getSyncedBooks(userId, lang);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// POST /book/sync/save - Save complete book
|
|
||||||
ipcMain.handle('db:book:syncSave', createHandler<CompleteBook, boolean>(
|
|
||||||
async function(userId: string, data: CompleteBook, lang: 'fr' | 'en'):Promise<boolean> {
|
|
||||||
return await Download.saveCompleteBook(userId, data, lang);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// GET /book/:id - Get single book
|
|
||||||
ipcMain.handle('db:book:bookBasicInformation', createHandler<string, BookProps>(
|
|
||||||
async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise<BookProps> {
|
|
||||||
return await Book.getBook(userId, bookId, lang);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// GET
|
|
||||||
ipcMain.handle('db:book:uploadToServer', createHandler<string, CompleteBook>(
|
|
||||||
async function(userId: string, bookId: string, lang: 'fr' | 'en'):Promise<CompleteBook> {
|
|
||||||
return await Upload.uploadBookForSync(userId, bookId, lang);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// POST /book/basic-information - Update book basic info
|
|
||||||
ipcMain.handle('db:book:updateBasicInformation', createHandler<UpdateBookBasicData, boolean>(
|
|
||||||
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<BookSyncCompare, CompleteBook>(
|
|
||||||
async function(userId: string, data:BookSyncCompare, lang: 'fr' | 'en'):Promise<CompleteBook> {
|
|
||||||
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<CompleteBook, boolean>(
|
|
||||||
async function(userId: string, data:CompleteBook, lang: 'fr' | 'en'):Promise<boolean> {
|
|
||||||
return await Sync.syncBookFromServerToClient(userId, data, lang);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// GET /book/guide-line - Get guideline
|
|
||||||
ipcMain.handle('db:book:guideline:get',
|
|
||||||
createHandler<GetGuidelineData, GuideLine | null>(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<UpdateGuideLineData, boolean>(
|
|
||||||
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<GetStoryData, StoryData>(
|
|
||||||
async function(userId: string, data: GetStoryData, lang: 'fr' | 'en'):Promise<StoryData> {
|
|
||||||
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<UpdateStoryData, boolean>(
|
|
||||||
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<CreateBookData, string>(
|
|
||||||
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<AddIncidentData, string>(
|
|
||||||
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<RemoveIncidentData, boolean>(
|
|
||||||
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<AddPlotPointData, string>(
|
|
||||||
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<RemovePlotData, boolean>(
|
|
||||||
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<AddIssueData, string>(
|
|
||||||
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<RemoveIssueData, boolean>(
|
|
||||||
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<GetWorldsData, WorldListResponse>(
|
|
||||||
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<AddWorldData, string>(
|
|
||||||
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<AddWorldElementData, string>(
|
|
||||||
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<RemoveWorldElementData, boolean>(
|
|
||||||
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<DeleteBookData, boolean>(
|
|
||||||
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<GetAIGuidelineData, GuideLineAI>(
|
|
||||||
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<SetAIGuideLineData, boolean>(
|
|
||||||
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<UpdateWorldData, boolean>(
|
|
||||||
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<UpdateBookToolData, boolean>(
|
|
||||||
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<ExportInfoData, ChapterExportInfo[]>(
|
|
||||||
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<ExportFormat, {ext: string; filterName: string}> = {
|
|
||||||
epub: {ext: 'epub', filterName: 'EPUB'},
|
|
||||||
pdf: {ext: 'pdf', filterName: 'PDF'},
|
|
||||||
docx: {ext: 'docx', filterName: 'Word Document'}
|
|
||||||
};
|
|
||||||
|
|
||||||
ipcMain.handle('db:book:export', createHandler<ExportRequestData, boolean>(
|
|
||||||
async function(userId: string, data: ExportRequestData, lang: 'fr' | 'en'): Promise<boolean> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@@ -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<string, ChapterProps[]>(
|
|
||||||
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<GetWholeChapterData, ChapterProps>(
|
|
||||||
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<string, ActStory[]>(
|
|
||||||
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<GetChapterContentData, CompanionContent>(
|
|
||||||
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<GetChapterContentData, string>(
|
|
||||||
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<SaveChapterContentData, boolean>(
|
|
||||||
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<string, ChapterProps | null>(
|
|
||||||
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<AddChapterData, string>(
|
|
||||||
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<RemoveChapterData, boolean>(
|
|
||||||
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<UpdateChapterData, boolean>(
|
|
||||||
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<AddChapterInformationData, string>(
|
|
||||||
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<RemoveChapterInfoData, boolean>(
|
|
||||||
function(userId: string, data: RemoveChapterInfoData, lang: 'fr' | 'en'): boolean {
|
|
||||||
return Chapter.removeChapterInformation(userId, data.bookId, data.chapterInfoId, data.deletedAt, lang);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@@ -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<GetCharacterListData, CharacterListResponse>(
|
|
||||||
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<GetCharacterAttributesData, CharacterAttribute[]>(
|
|
||||||
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<AddCharacterData, string>(
|
|
||||||
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<AddAttributeData, string>(
|
|
||||||
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<DeleteAttributeData, boolean>(
|
|
||||||
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<UpdateCharacterData, boolean>(
|
|
||||||
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<DeleteCharacterData, boolean>(
|
|
||||||
function(userId: string, data: DeleteCharacterData, lang: 'fr' | 'en'): boolean {
|
|
||||||
return Character.deleteCharacter(userId, data.bookId, data.characterId, data.deletedAt, lang);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@@ -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<GetAllLocationsData, LocationListResponse>(
|
|
||||||
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<AddLocationSectionData, string>(
|
|
||||||
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<AddLocationElementData, string>(
|
|
||||||
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<AddLocationSubElementData, string>(
|
|
||||||
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<UpdateLocationData, UpdateLocationResponse>(
|
|
||||||
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<UpdateSectionWithSeriesLinkData, boolean>(
|
|
||||||
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<DeleteLocationData, boolean>(
|
|
||||||
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<DeleteLocationElementData, boolean>(
|
|
||||||
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<DeleteLocationSubElementData, boolean>(
|
|
||||||
function(userId: string, data: DeleteLocationSubElementData, lang: 'fr' | 'en'): boolean {
|
|
||||||
return Location.deleteLocationSubElement(userId, data.bookId, data.subElementId, data.deletedAt, lang);
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
@@ -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<string>('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<string>('lastUserId');
|
|
||||||
if (!lastUserId) {
|
|
||||||
return { success: false, error: 'No offline account found' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const hashedPin = storage.get<string>(`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<string>(`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<boolean>('offlineMode', false);
|
|
||||||
const syncInterval = storage.get<number>('syncInterval', 30);
|
|
||||||
const lastUserId = storage.get<string>('lastUserId');
|
|
||||||
const hasPin = lastUserId ? !!storage.get<string>(`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<string>('lastSync');
|
|
||||||
const syncInterval = storage.get<number>('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 };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -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<GetCharacterListData, SeriesCharacterListProps[]>(
|
|
||||||
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<GetCharacterAttributesData, CharacterAttributesResponse>(
|
|
||||||
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<AddCharacterData, string>(
|
|
||||||
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<UpdateCharacterData, boolean>(
|
|
||||||
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<DeleteCharacterData, boolean>(
|
|
||||||
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<AddAttributeData, string>(
|
|
||||||
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<DeleteAttributeData, boolean>(
|
|
||||||
function(userId: string, data: DeleteAttributeData, lang: 'fr' | 'en'): boolean {
|
|
||||||
return SeriesCharacter.deleteAttribute(userId, data.attributeId, data.deletedAt, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
@@ -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<GetLocationListData, SeriesLocationListProps[]>(
|
|
||||||
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<AddLocationSectionData, string>(
|
|
||||||
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<AddElementData, string>(
|
|
||||||
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<AddSubElementData, string>(
|
|
||||||
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<DeleteLocationData, boolean>(
|
|
||||||
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<DeleteElementData, boolean>(
|
|
||||||
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<DeleteSubElementData, boolean>(
|
|
||||||
function(userId: string, data: DeleteSubElementData, lang: 'fr' | 'en'): boolean {
|
|
||||||
return SeriesLocation.deleteSubElement(userId, data.subElementId, data.deletedAt, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
@@ -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<GetSpellListData, SeriesSpellListResponse>(
|
|
||||||
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<GetSpellDetailData, SeriesSpellDetailProps>(
|
|
||||||
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<AddSpellData, string>(
|
|
||||||
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<UpdateSpellData, boolean>(
|
|
||||||
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<DeleteSpellData, boolean>(
|
|
||||||
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<AddTagData, string>(
|
|
||||||
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<UpdateTagData, boolean>(
|
|
||||||
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<DeleteTagData, boolean>(
|
|
||||||
function(userId: string, data: DeleteTagData, lang: 'fr' | 'en'): boolean {
|
|
||||||
return SeriesSpell.deleteTag(userId, data.tagId, data.deletedAt, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
@@ -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<UploadToSeriesData, SeriesSyncResult>(
|
|
||||||
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<void, SyncedSeries[]>(
|
|
||||||
function(userId: string, _data: void, lang: 'fr' | 'en'): SyncedSeries[] {
|
|
||||||
return SeriesSync.getSyncedSeries(userId, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
ipcMain.handle('db:series:uploadToServer', createHandler<string, CompleteSeries>(
|
|
||||||
async function(userId: string, seriesId: string, lang: 'fr' | 'en'): Promise<CompleteSeries> {
|
|
||||||
return SeriesSync.getCompleteSeriesForUpload(userId, seriesId, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
ipcMain.handle('db:series:syncSave', createHandler<CompleteSeries, boolean>(
|
|
||||||
async function(userId: string, completeSeries: CompleteSeries, lang: 'fr' | 'en'): Promise<boolean> {
|
|
||||||
return SeriesSync.saveCompleteSeries(userId, completeSeries, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
ipcMain.handle('db:series:sync:toClient', createHandler<CompleteSeries, boolean>(
|
|
||||||
async function(userId: string, completeSeries: CompleteSeries, lang: 'fr' | 'en'): Promise<boolean> {
|
|
||||||
return SeriesSync.syncSeriesFromServerToClient(userId, completeSeries, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
ipcMain.handle('db:series:sync:toServer', createHandler<object, CompleteSeries>(
|
|
||||||
async function(userId: string, syncCompare: object, lang: 'fr' | 'en'): Promise<CompleteSeries> {
|
|
||||||
const seriesId = (syncCompare as { id: string }).id;
|
|
||||||
return SeriesSync.getCompleteSeriesForUpload(userId, seriesId, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
@@ -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<GetWorldListData, SeriesWorldListProps[]>(
|
|
||||||
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<AddWorldData, string>(
|
|
||||||
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<UpdateWorldData, boolean>(
|
|
||||||
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<AddElementData, string>(
|
|
||||||
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<DeleteElementData, boolean>(
|
|
||||||
function(userId: string, data: DeleteElementData, lang: 'fr' | 'en'): boolean {
|
|
||||||
return SeriesWorld.deleteElement(userId, data.elementId, data.deletedAt, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
@@ -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<void, SeriesListItemProps[]>(
|
|
||||||
async function(userId: string, _body: void, lang: 'fr' | 'en'): Promise<SeriesListItemProps[]> {
|
|
||||||
return await Series.getSeriesList(userId, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
// GET /series/detail - Get series detail
|
|
||||||
ipcMain.handle('db:series:detail', createHandler<GetSeriesDetailData, SeriesDetailProps>(
|
|
||||||
async function(userId: string, data: GetSeriesDetailData, lang: 'fr' | 'en'): Promise<SeriesDetailProps> {
|
|
||||||
return await Series.getSeriesDetail(userId, data.seriesId, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
// POST /series/add - Create new series
|
|
||||||
ipcMain.handle('db:series:create', createHandler<CreateSeriesData, string>(
|
|
||||||
async function(userId: string, data: CreateSeriesData, lang: 'fr' | 'en'): Promise<string> {
|
|
||||||
return await Series.createSeries(userId, data.name, data.description || '', lang, data.bookIds);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
// PUT /series/update - Update series
|
|
||||||
ipcMain.handle('db:series:update', createHandler<UpdateSeriesData, boolean>(
|
|
||||||
async function(userId: string, data: UpdateSeriesData, lang: 'fr' | 'en'): Promise<boolean> {
|
|
||||||
return await Series.updateSeries(userId, data.seriesId, data.name, data.description || '', lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
// DELETE /series/delete - Delete series
|
|
||||||
ipcMain.handle('db:series:delete', createHandler<DeleteSeriesData, boolean>(
|
|
||||||
async function(userId: string, data: DeleteSeriesData, lang: 'fr' | 'en'): Promise<boolean> {
|
|
||||||
return await Series.deleteSeries(userId, data.seriesId, data.deletedAt, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
// GET /series/book/list - Get books in series
|
|
||||||
ipcMain.handle('db:series:books', createHandler<GetSeriesBooksData, SeriesBookProps[]>(
|
|
||||||
async function(userId: string, data: GetSeriesBooksData, lang: 'fr' | 'en'): Promise<SeriesBookProps[]> {
|
|
||||||
return await Series.getSeriesBooks(userId, data.seriesId, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
// POST /series/book/add - Add book to series
|
|
||||||
ipcMain.handle('db:series:book:add', createHandler<AddBookToSeriesData, boolean>(
|
|
||||||
async function(userId: string, data: AddBookToSeriesData, lang: 'fr' | 'en'): Promise<boolean> {
|
|
||||||
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<RemoveBookFromSeriesData, boolean>(
|
|
||||||
async function(userId: string, data: RemoveBookFromSeriesData, lang: 'fr' | 'en'): Promise<boolean> {
|
|
||||||
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<UpdateBooksOrderData, boolean>(
|
|
||||||
async function(userId: string, data: UpdateBooksOrderData, lang: 'fr' | 'en'): Promise<boolean> {
|
|
||||||
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<GetSeriesForBookData, string | null>(
|
|
||||||
function(_userId: string, data: GetSeriesForBookData, _lang: 'fr' | 'en'): string | null {
|
|
||||||
return Series.getSeriesIdForBook(data.bookId);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
@@ -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<GetSpellListData, SpellListResponse>(
|
|
||||||
function (userId: string, data: GetSpellListData, lang: 'fr' | 'en'): SpellListResponse {
|
|
||||||
return Spell.getSpellList(userId, data.bookid, lang);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// GET /spell/tags
|
|
||||||
ipcMain.handle(
|
|
||||||
'db:spell:tags',
|
|
||||||
createHandler<GetSpellTagsData, SpellTagProps[]>(
|
|
||||||
function (userId: string, data: GetSpellTagsData, lang: 'fr' | 'en'): SpellTagProps[] {
|
|
||||||
return Spell.getSpellTags(userId, data.bookid, lang);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// GET /spell/detail
|
|
||||||
ipcMain.handle(
|
|
||||||
'db:spell:detail',
|
|
||||||
createHandler<GetSpellDetailData, SpellProps>(
|
|
||||||
function (userId: string, data: GetSpellDetailData, lang: 'fr' | 'en'): SpellProps {
|
|
||||||
return Spell.getSpellDetail(userId, data.spellid, lang);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// POST /spell/add
|
|
||||||
ipcMain.handle(
|
|
||||||
'db:spell:create',
|
|
||||||
createHandler<CreateSpellData, string>(
|
|
||||||
function (userId: string, data: CreateSpellData, lang: 'fr' | 'en'): string {
|
|
||||||
const spell: SpellPost = data.spell;
|
|
||||||
const result: SpellProps = Spell.addSpell(
|
|
||||||
userId,
|
|
||||||
data.bookId,
|
|
||||||
spell.name,
|
|
||||||
spell.description,
|
|
||||||
spell.appearance,
|
|
||||||
spell.tags || [],
|
|
||||||
spell.powerLevel || null,
|
|
||||||
spell.components || null,
|
|
||||||
spell.limitations || null,
|
|
||||||
spell.notes || null,
|
|
||||||
spell.id,
|
|
||||||
lang,
|
|
||||||
spell.seriesSpellId || null,
|
|
||||||
);
|
|
||||||
return result.id;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// PUT /spell/update
|
|
||||||
ipcMain.handle(
|
|
||||||
'db:spell:update',
|
|
||||||
createHandler<UpdateSpellData, boolean>(
|
|
||||||
function (userId: string, data: UpdateSpellData, lang: 'fr' | 'en'): boolean {
|
|
||||||
const spell: SpellPost = data.spell;
|
|
||||||
return Spell.updateSpell(
|
|
||||||
userId,
|
|
||||||
data.spellId,
|
|
||||||
spell.name,
|
|
||||||
spell.description,
|
|
||||||
spell.appearance,
|
|
||||||
spell.tags || [],
|
|
||||||
spell.powerLevel || null,
|
|
||||||
spell.components || null,
|
|
||||||
spell.limitations || null,
|
|
||||||
spell.notes || null,
|
|
||||||
lang,
|
|
||||||
spell.seriesSpellId || null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// DELETE /spell/delete
|
|
||||||
ipcMain.handle(
|
|
||||||
'db:spell:delete',
|
|
||||||
createHandler<DeleteSpellData, boolean>(
|
|
||||||
function (userId: string, data: DeleteSpellData, lang: 'fr' | 'en'): boolean {
|
|
||||||
return Spell.deleteSpell(userId, data.bookId, data.spellId, data.deletedAt, lang);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ==================== SPELL TAG HANDLERS ====================
|
|
||||||
|
|
||||||
// POST /spell/tag/add
|
|
||||||
ipcMain.handle(
|
|
||||||
'db:spell:tag:create',
|
|
||||||
createHandler<CreateTagData, string>(
|
|
||||||
function (userId: string, data: CreateTagData, lang: 'fr' | 'en'): string {
|
|
||||||
const result: SpellTagProps = Spell.addSpellTag(
|
|
||||||
userId,
|
|
||||||
data.bookId,
|
|
||||||
data.name,
|
|
||||||
data.color || null,
|
|
||||||
undefined,
|
|
||||||
lang,
|
|
||||||
);
|
|
||||||
return result.id;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// PUT /spell/tag/update
|
|
||||||
ipcMain.handle(
|
|
||||||
'db:spell:tag:update',
|
|
||||||
createHandler<UpdateTagData, boolean>(
|
|
||||||
function (userId: string, data: UpdateTagData, lang: 'fr' | 'en'): boolean {
|
|
||||||
return Spell.updateSpellTag(
|
|
||||||
userId,
|
|
||||||
data.tagId,
|
|
||||||
data.name,
|
|
||||||
data.color || null,
|
|
||||||
lang,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// DELETE /spell/tag/delete
|
|
||||||
ipcMain.handle(
|
|
||||||
'db:spell:tag:delete',
|
|
||||||
createHandler<DeleteTagData, boolean>(
|
|
||||||
function (userId: string, data: DeleteTagData, lang: 'fr' | 'en'): boolean {
|
|
||||||
return Spell.deleteSpellTag(userId, data.bookId, data.tagId, data.deletedAt, lang);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
@@ -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<number, RemovedItemRecord[]>(
|
|
||||||
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<RemovedItemRecord[], void>(
|
|
||||||
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<RemovedItemRecord[], void>(
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
@@ -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<void, UserInfoResponse>(
|
|
||||||
async function(userId: string, _body: void, _lang: 'fr' | 'en'):Promise<UserInfoResponse> {
|
|
||||||
return await User.returnUserInfos(userId);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
// PUT /user/update - Update user info in local DB
|
|
||||||
ipcMain.handle('db:user:update', createHandler<UpdateUserData, boolean>(
|
|
||||||
async function(userId: string, data: UpdateUserData, lang: 'fr' | 'en'):Promise<boolean> {
|
|
||||||
const userKey = '';
|
|
||||||
return await User.updateUserInfos(userKey, userId, data.firstName, data.lastName, data.username, data.email, data.authorName, lang);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
826
electron/main.ts
826
electron/main.ts
@@ -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<OAuthResult> => {
|
|
||||||
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<string, string> = {
|
|
||||||
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<void> => {
|
|
||||||
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<string>('userId', null);
|
|
||||||
const lastUserId:string | null = storage.get<string>('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<string>(`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<boolean> => {
|
|
||||||
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<string>('userId');
|
|
||||||
const lastUserId = storage.get<string>('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<string, string> = {
|
|
||||||
'.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();
|
|
||||||
});
|
|
||||||
@@ -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'),
|
|
||||||
});
|
|
||||||
@@ -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<string, string>;
|
|
||||||
|
|
||||||
class SecureStorage {
|
|
||||||
private readonly storePath: string;
|
|
||||||
private readonly cache: Map<string, StorageValue> = 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<T = string>(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;
|
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>ERitors Scribe</title>
|
||||||
|
<link rel="icon" href="/eritors-favicon-white.png"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/app/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,104 +1,93 @@
|
|||||||
import axios, {AxiosResponse, Method} from "axios";
|
import {fetch} from "@tauri-apps/plugin-http";
|
||||||
import {configs, isDesktop} from "@/lib/configs";
|
import {configs} from "@/lib/configs";
|
||||||
|
|
||||||
type ContentType = 'application/json' | 'multipart/form-data';
|
export class ApiError extends Error {
|
||||||
|
statusCode: number;
|
||||||
interface ApiRequestConfig {
|
constructor(message: string, statusCode: number) {
|
||||||
method: Method;
|
super(message);
|
||||||
url: string;
|
this.statusCode = statusCode;
|
||||||
auth: string;
|
this.name = 'ApiError';
|
||||||
lang?: string;
|
}
|
||||||
params?: Record<string, unknown>;
|
}
|
||||||
data?: unknown;
|
|
||||||
contentType?: ContentType;
|
function buildUrl(url: string, params: Record<string, unknown> = {}, lang: string = "fr"): string {
|
||||||
}
|
const fullUrl = new URL(url, configs.apiUrl);
|
||||||
|
fullUrl.searchParams.set("lang", lang);
|
||||||
export class ApiError extends Error {
|
fullUrl.searchParams.set("plateforme", "desktop");
|
||||||
statusCode: number;
|
for (const [key, value] of Object.entries(params)) {
|
||||||
constructor(message: string, statusCode: number) {
|
if (value !== undefined && value !== null) fullUrl.searchParams.set(key, String(value));
|
||||||
super(message);
|
}
|
||||||
this.statusCode = statusCode;
|
return fullUrl.toString();
|
||||||
this.name = 'ApiError';
|
}
|
||||||
}
|
|
||||||
}
|
async function handleResponse<T>(response: Response): Promise<T> {
|
||||||
|
if (!response.ok) {
|
||||||
function handleApiError(error: unknown): never {
|
const body = await response.json().catch(() => ({message: response.statusText}));
|
||||||
if (axios.isAxiosError(error)) {
|
throw new ApiError(body.message || body || response.statusText, response.status);
|
||||||
const serverMessage: string = error.response?.data?.message || error.response?.data || error.message;
|
}
|
||||||
const statusCode: number = error.response?.status ?? 500;
|
return response.json() as Promise<T>;
|
||||||
throw new ApiError(serverMessage, statusCode);
|
}
|
||||||
} else if (error instanceof Error) {
|
|
||||||
throw new Error(error.message);
|
export async function apiGet<T>(url: string, auth: string, lang: string = "fr", params: Record<string, unknown> = {}): Promise<T> {
|
||||||
}
|
const response = await fetch(buildUrl(url, params, lang), {
|
||||||
throw new Error('An unexpected error occurred');
|
method: "GET",
|
||||||
}
|
headers: {"Authorization": `Bearer ${auth}`},
|
||||||
|
});
|
||||||
async function apiRequest<T>(config: ApiRequestConfig): Promise<T> {
|
return handleResponse<T>(response);
|
||||||
try {
|
}
|
||||||
const headers: Record<string, string> = {
|
|
||||||
'Authorization': `Bearer ${config.auth}`
|
export async function apiPost<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
|
||||||
};
|
const response = await fetch(buildUrl(url, {}, lang), {
|
||||||
|
method: "POST",
|
||||||
if (config.contentType) {
|
headers: {"Authorization": `Bearer ${auth}`, "Content-Type": "application/json"},
|
||||||
headers['Content-Type'] = config.contentType;
|
body: JSON.stringify(data),
|
||||||
}
|
});
|
||||||
|
return handleResponse<T>(response);
|
||||||
const response: AxiosResponse<T> = await axios({
|
}
|
||||||
method: config.method,
|
|
||||||
headers,
|
export async function apiPut<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
|
||||||
params: {
|
const response = await fetch(buildUrl(url, {}, lang), {
|
||||||
lang: config.lang ?? 'fr',
|
method: "PUT",
|
||||||
plateforme: isDesktop ? 'desktop' : 'web',
|
headers: {"Authorization": `Bearer ${auth}`, "Content-Type": "application/json"},
|
||||||
...config.params
|
body: JSON.stringify(data),
|
||||||
},
|
});
|
||||||
url: configs.apiUrl + config.url,
|
return handleResponse<T>(response);
|
||||||
data: config.data
|
}
|
||||||
});
|
|
||||||
|
export async function apiPatch<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
|
||||||
return response.data;
|
const response = await fetch(buildUrl(url, {}, lang), {
|
||||||
} catch (error: unknown) {
|
method: "PATCH",
|
||||||
handleApiError(error);
|
headers: {"Authorization": `Bearer ${auth}`, "Content-Type": "application/json"},
|
||||||
}
|
body: JSON.stringify(data),
|
||||||
}
|
});
|
||||||
|
return handleResponse<T>(response);
|
||||||
export async function apiGet<T>(url: string, auth: string, lang: string = "fr", params: Record<string, unknown> = {}): Promise<T> {
|
}
|
||||||
return apiRequest<T>({method: 'GET', url, auth, lang, params});
|
|
||||||
}
|
export async function apiDelete<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
|
||||||
|
const response = await fetch(buildUrl(url, {}, lang), {
|
||||||
export async function apiPost<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
|
method: "DELETE",
|
||||||
return apiRequest<T>({method: 'POST', url, auth, lang, data, contentType: 'application/json'});
|
headers: {"Authorization": `Bearer ${auth}`, "Content-Type": "application/json"},
|
||||||
}
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
export async function apiPut<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
|
return handleResponse<T>(response);
|
||||||
return apiRequest<T>({method: 'PUT', url, auth, lang, data, contentType: 'application/json'});
|
}
|
||||||
}
|
|
||||||
|
export async function apiPostPublic<T>(url: string, data: object, lang: string = "fr"): Promise<T> {
|
||||||
export async function apiPatch<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
|
const response = await fetch(buildUrl(url, {}, lang), {
|
||||||
return apiRequest<T>({method: 'PATCH', url, auth, lang, data, contentType: 'application/json'});
|
method: "POST",
|
||||||
}
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify(data),
|
||||||
export async function apiDelete<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
|
});
|
||||||
return apiRequest<T>({method: 'DELETE', url, auth, lang, data, contentType: 'application/json'});
|
return handleResponse<T>(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apiPostPublic<T>(url: string, data: object, lang: string = "fr"): Promise<T> {
|
export async function apiUpload<T>(url: string, file: File, auth: string, lang: string = "fr"): Promise<T> {
|
||||||
try {
|
const formData = new FormData();
|
||||||
const response: AxiosResponse<T> = await axios({
|
formData.append("file", file);
|
||||||
method: 'POST',
|
const response = await fetch(buildUrl(url, {}, lang), {
|
||||||
headers: {'Content-Type': 'application/json'},
|
method: "POST",
|
||||||
params: {lang, plateforme: isDesktop ? 'desktop' : 'web'},
|
headers: {"Authorization": `Bearer ${auth}`},
|
||||||
url: configs.apiUrl + url,
|
body: formData,
|
||||||
data
|
});
|
||||||
});
|
return handleResponse<T>(response);
|
||||||
return response.data;
|
}
|
||||||
} catch (error: unknown) {
|
|
||||||
handleApiError(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function apiUpload<T>(url: string, file: File, auth: string, lang: string = "fr"): Promise<T> {
|
|
||||||
const formData: FormData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
return apiRequest<T>({method: 'POST', url, auth, lang, data: formData, contentType: 'multipart/form-data'});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export interface Configs {
|
|||||||
appVersion: string;
|
appVersion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isProduction: boolean = false;
|
const isProduction: boolean = true;
|
||||||
export const isDesktop: boolean = true;
|
export const isDesktop: boolean = true;
|
||||||
|
|
||||||
export const configs: Configs = {
|
export const configs: Configs = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import axios from 'axios';
|
import {fetch} from '@tauri-apps/plugin-http';
|
||||||
import {configs, isDesktop} from '@/lib/configs';
|
import {configs, isDesktop} from '@/lib/configs';
|
||||||
|
|
||||||
interface CrashReportPayload {
|
interface CrashReportPayload {
|
||||||
@@ -49,9 +49,10 @@ function getUserId(): string | undefined {
|
|||||||
|
|
||||||
async function sendCrashReport(payload: CrashReportPayload): Promise<void> {
|
async function sendCrashReport(payload: CrashReportPayload): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await axios.post(configs.apiUrl + 'crash-report', payload, {
|
await fetch(configs.apiUrl + 'crash-report', {
|
||||||
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
timeout: 5000,
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// Silently fail — we can't crash while reporting a crash
|
// Silently fail — we can't crash while reporting a crash
|
||||||
|
|||||||
@@ -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.",
|
"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.",
|
"emptyPromptError": "Please enter a prompt to get inspired.",
|
||||||
"error": {
|
"error": {
|
||||||
"contentRetrieval": "Error retrieving content.",
|
|
||||||
"contentRetrievalUnknown": "Unknown error retrieving content.",
|
"contentRetrievalUnknown": "Unknown error retrieving content.",
|
||||||
"noBook": "No book selected.",
|
"noBook": "No book selected.",
|
||||||
"noChapter": "No chapter selected.",
|
"noChapter": "No chapter selected.",
|
||||||
@@ -669,7 +668,6 @@
|
|||||||
"successSaveAdvanced": "Advanced settings saved successfully.",
|
"successSaveAdvanced": "Advanced settings saved successfully.",
|
||||||
"errorSave": "An error occurred during saving.",
|
"errorSave": "An error occurred during saving.",
|
||||||
"errorUnknownSave": "An unknown error occurred during saving.",
|
"errorUnknownSave": "An unknown error occurred during saving.",
|
||||||
"errorRetrieveContent": "Error retrieving content.",
|
|
||||||
"errorUnknownRetrieveContent": "Unknown error retrieving content.",
|
"errorUnknownRetrieveContent": "Unknown error retrieving content.",
|
||||||
"abortSuccess": "Generation stopped. Token and cost totals will be available on next page refresh.",
|
"abortSuccess": "Generation stopped. Token and cost totals will be available on next page refresh.",
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1286,7 +1284,6 @@
|
|||||||
"emailLength": "Email address must be between 5 and 100 characters.",
|
"emailLength": "Email address must be between 5 and 100 characters.",
|
||||||
"emailInvalidChars": "Email address contains invalid characters.",
|
"emailInvalidChars": "Email address contains invalid characters.",
|
||||||
"connection": "Connection error. Check your credentials.",
|
"connection": "Connection error. Check your credentials.",
|
||||||
"server": "Server error. Please try again later.",
|
|
||||||
"unknown": "An unknown error occurred."
|
"unknown": "An unknown error occurred."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1402,11 +1399,8 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"emailInvalid": "Please enter a valid email address.",
|
"emailInvalid": "Please enter a valid email address.",
|
||||||
"emailFormat": "The email address format is invalid.",
|
"emailFormat": "The email address format is invalid.",
|
||||||
"emailServer": "Server error while verifying email.",
|
|
||||||
"emailUnknown": "An unknown error occurred.",
|
"emailUnknown": "An unknown error occurred.",
|
||||||
"codeServer": "Server error while verifying code.",
|
|
||||||
"codeUnknown": "An unknown error occurred.",
|
"codeUnknown": "An unknown error occurred.",
|
||||||
"passwordServer": "Server error while changing password.",
|
|
||||||
"passwordUnknown": "An unknown error occurred."
|
"passwordUnknown": "An unknown error occurred."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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.",
|
"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.",
|
"emptyPromptError": "Veuillez entrer un prompt pour vous inspirer.",
|
||||||
"error": {
|
"error": {
|
||||||
"contentRetrieval": "Erreur lors de la récupération du contenu.",
|
|
||||||
"contentRetrievalUnknown": "Erreur inconnue lors de la récupération du contenu.",
|
"contentRetrievalUnknown": "Erreur inconnue lors de la récupération du contenu.",
|
||||||
"noBook": "Aucun livre sélectionné.",
|
"noBook": "Aucun livre sélectionné.",
|
||||||
"noChapter": "Aucun chapitre sélectionné.",
|
"noChapter": "Aucun chapitre sélectionné.",
|
||||||
@@ -669,7 +668,6 @@
|
|||||||
"successSaveAdvanced": "Paramètres avancés sauvegardés avec succès.",
|
"successSaveAdvanced": "Paramètres avancés sauvegardés avec succès.",
|
||||||
"errorSave": "Une erreur est survenue pendant la sauvegarde.",
|
"errorSave": "Une erreur est survenue pendant la sauvegarde.",
|
||||||
"errorUnknownSave": "Une erreur inconnue 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.",
|
"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.",
|
"abortSuccess": "Génération arrêtée. Les totaux de tokens et coûts seront disponibles au prochain rafraîchissement de la page.",
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -1285,7 +1283,6 @@
|
|||||||
"emailLength": "L'adresse e-mail doit contenir entre 5 et 100 caractères.",
|
"emailLength": "L'adresse e-mail doit contenir entre 5 et 100 caractères.",
|
||||||
"emailInvalidChars": "L'adresse e-mail contient des caractères invalides.",
|
"emailInvalidChars": "L'adresse e-mail contient des caractères invalides.",
|
||||||
"connection": "Erreur de connexion. Vérifiez vos identifiants.",
|
"connection": "Erreur de connexion. Vérifiez vos identifiants.",
|
||||||
"server": "Erreur du serveur. Veuillez réessayer plus tard.",
|
|
||||||
"unknown": "Une erreur inconnue est survenue."
|
"unknown": "Une erreur inconnue est survenue."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1401,11 +1398,8 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"emailInvalid": "Veuillez entrer une adresse e-mail valide.",
|
"emailInvalid": "Veuillez entrer une adresse e-mail valide.",
|
||||||
"emailFormat": "Le format de l'adresse e-mail est invalide.",
|
"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.",
|
"emailUnknown": "Une erreur inconnue est survenue.",
|
||||||
"codeServer": "Erreur du serveur lors de la vérification du code.",
|
|
||||||
"codeUnknown": "Une erreur inconnue est survenue.",
|
"codeUnknown": "Une erreur inconnue est survenue.",
|
||||||
"passwordServer": "Erreur du serveur lors du changement de mot de passe.",
|
|
||||||
"passwordUnknown": "Une erreur inconnue est survenue."
|
"passwordUnknown": "Une erreur inconnue est survenue."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
|
||||||
4267
package-lock.json
generated
4267
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
76
package.json
76
package.json
@@ -3,20 +3,19 @@
|
|||||||
"productName": "ERitors Scribe",
|
"productName": "ERitors Scribe",
|
||||||
"version": "0.5.0",
|
"version": "0.5.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/electron/main.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 4000",
|
"dev": "vite --port 4000",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri:dev": "tauri dev",
|
"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": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron/notarize": "^3.1.1",
|
|
||||||
"@tauri-apps/cli": "^2.10.1",
|
"@tauri-apps/cli": "^2.10.1",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
@@ -25,14 +24,14 @@
|
|||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"electron": "^39.2.1",
|
"dotenv-cli": "^11.0.0",
|
||||||
"electron-builder": "^26.0.12",
|
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"wait-on": "^9.0.3"
|
"wait-on": "^9.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@tauri-apps/api": "^2.10.1",
|
"@tauri-apps/api": "^2.10.1",
|
||||||
|
"@tauri-apps/plugin-http": "^2.5.8",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||||
"@tiptap/extension-color": "^3.10.7",
|
"@tiptap/extension-color": "^3.10.7",
|
||||||
"@tiptap/extension-gapcursor": "^3.10.7",
|
"@tiptap/extension-gapcursor": "^3.10.7",
|
||||||
@@ -41,17 +40,12 @@
|
|||||||
"@tiptap/extension-underline": "^3.10.7",
|
"@tiptap/extension-underline": "^3.10.7",
|
||||||
"@tiptap/react": "^3.10.7",
|
"@tiptap/react": "^3.10.7",
|
||||||
"@tiptap/starter-kit": "^3.10.7",
|
"@tiptap/starter-kit": "^3.10.7",
|
||||||
"@types/bcrypt": "^6.0.0",
|
|
||||||
"@vitejs/plugin-react": "^6.0.1",
|
"@vitejs/plugin-react": "^6.0.1",
|
||||||
"autoprefixer": "^10.4.22",
|
"autoprefixer": "^10.4.22",
|
||||||
"axios": "^1.13.2",
|
|
||||||
"bcrypt": "^6.0.0",
|
|
||||||
"docx": "^9.5.3",
|
"docx": "^9.5.3",
|
||||||
"electron-updater": "^6.7.3",
|
|
||||||
"i18next": "^25.10.4",
|
"i18next": "^25.10.4",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"lucide-react": "^0.577.0",
|
"lucide-react": "^0.577.0",
|
||||||
"node-sqlite3-wasm": "^0.8.51",
|
|
||||||
"pdfkit": "^0.17.2",
|
"pdfkit": "^0.17.2",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
@@ -60,67 +54,5 @@
|
|||||||
"react-router-dom": "^7.13.1",
|
"react-router-dom": "^7.13.1",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
"vite": "^8.0.1"
|
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,243 +1,274 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
|
use serde::{Deserialize, Serialize};
|
||||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
|
|
||||||
use rand::RngCore;
|
use crate::error::{AppError, AppResult};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
const SERVICE_NAME: &str = "com.eritors.scribe.desktop";
|
||||||
use crate::error::{AppError, AppResult};
|
|
||||||
|
// ===== DEV: plain JSON vault (everything in one file, no encryption) =====
|
||||||
const SERVICE_NAME: &str = "com.eritors.scribe.desktop";
|
|
||||||
const IV_LENGTH: usize = 16;
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
|
struct SecureVault {
|
||||||
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
|
token: Option<String>,
|
||||||
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
|
encryption_keys: HashMap<String, String>,
|
||||||
|
last_user_id: Option<String>,
|
||||||
// ===== Secure vault: encrypted JSON file (like Electron's safeStorage) =====
|
pin_hashes: HashMap<String, String>,
|
||||||
|
#[serde(default)]
|
||||||
#[derive(Serialize, Deserialize, Default)]
|
pin_failed_attempts: i32,
|
||||||
struct SecureVault {
|
#[serde(default)]
|
||||||
token: Option<String>,
|
pin_locked_until: Option<i64>,
|
||||||
encryption_keys: HashMap<String, String>,
|
}
|
||||||
last_user_id: Option<String>,
|
|
||||||
pin_hashes: HashMap<String, String>,
|
// ===== PROD: plain JSON for non-sensitive config only =====
|
||||||
#[serde(default)]
|
|
||||||
pin_failed_attempts: i32,
|
#[derive(Serialize, Deserialize, Default)]
|
||||||
#[serde(default)]
|
struct AppConfig {
|
||||||
pin_locked_until: Option<i64>,
|
last_user_id: Option<String>,
|
||||||
}
|
#[serde(default)]
|
||||||
|
pin_failed_attempts: i32,
|
||||||
fn vault_path() -> PathBuf {
|
#[serde(default)]
|
||||||
dirs_next::data_dir()
|
pin_locked_until: Option<i64>,
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
}
|
||||||
.join(SERVICE_NAME)
|
|
||||||
.join("secure-config.json")
|
fn config_path() -> PathBuf {
|
||||||
}
|
dirs_next::data_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("."))
|
||||||
const KEYRING_USER: &str = "vault-key";
|
.join(SERVICE_NAME)
|
||||||
|
.join(if cfg!(debug_assertions) { "secure-config.json" } else { "app-config.json" })
|
||||||
/// 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,
|
// ── DEV helpers ──────────────────────────────────────────────────────────
|
||||||
/// and attempts to migrate the key into the keyring for next time.
|
|
||||||
fn get_vault_key() -> [u8; 32] {
|
fn read_vault() -> AppResult<SecureVault> {
|
||||||
let entry = keyring::Entry::new(SERVICE_NAME, KEYRING_USER);
|
let path = config_path();
|
||||||
if let Ok(entry) = &entry {
|
if !path.exists() { return Ok(SecureVault::default()); }
|
||||||
if let Ok(stored) = entry.get_password() {
|
let content = fs::read_to_string(&path)
|
||||||
if let Ok(decoded) = BASE64.decode(stored.trim()) {
|
.map_err(|e| AppError::Keyring(format!("Failed to read vault: {}", e)))?;
|
||||||
if decoded.len() == 32 {
|
serde_json::from_str::<SecureVault>(&content)
|
||||||
let mut key = [0u8; 32];
|
.map_err(|e| AppError::Keyring(format!("Vault JSON parse error: {}", e)))
|
||||||
key.copy_from_slice(&decoded);
|
}
|
||||||
return key;
|
|
||||||
}
|
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 mut key = [0u8; 32];
|
}
|
||||||
rand::rng().fill_bytes(&mut key);
|
let json = serde_json::to_string_pretty(vault)
|
||||||
let encoded = BASE64.encode(key);
|
.map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?;
|
||||||
if let Ok(entry) = &entry {
|
fs::write(&path, json).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e)))
|
||||||
let _ = entry.set_password(&encoded);
|
}
|
||||||
}
|
|
||||||
key
|
// ── PROD helpers ─────────────────────────────────────────────────────────
|
||||||
}
|
|
||||||
|
fn read_config() -> AppResult<AppConfig> {
|
||||||
fn encrypt_vault(data: &[u8], key: &[u8; 32]) -> AppResult<Vec<u8>> {
|
let path = config_path();
|
||||||
let mut iv = [0u8; IV_LENGTH];
|
if !path.exists() { return Ok(AppConfig::default()); }
|
||||||
rand::rng().fill_bytes(&mut iv);
|
let content = fs::read_to_string(&path)
|
||||||
let block_size = 16;
|
.map_err(|e| AppError::Keyring(format!("Failed to read config: {}", e)))?;
|
||||||
let padding_len = block_size - (data.len() % block_size);
|
serde_json::from_str::<AppConfig>(&content)
|
||||||
let mut buf = Vec::with_capacity(data.len() + padding_len);
|
.map_err(|e| AppError::Keyring(format!("Config JSON parse error: {}", e)))
|
||||||
buf.extend_from_slice(data);
|
}
|
||||||
buf.extend(std::iter::repeat(padding_len as u8).take(padding_len));
|
|
||||||
let padded_len = buf.len();
|
fn write_config(config: &AppConfig) -> AppResult<()> {
|
||||||
let cipher = Aes256CbcEnc::new(key.into(), &iv.into());
|
let path = config_path();
|
||||||
cipher.encrypt_padded_mut::<aes::cipher::block_padding::NoPadding>(&mut buf, padded_len)
|
if let Some(parent) = path.parent() {
|
||||||
.map_err(|e| AppError::Encryption(format!("Vault encrypt failed: {}", e)))?;
|
fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create dir: {}", e)))?;
|
||||||
let mut result = Vec::with_capacity(IV_LENGTH + buf.len());
|
}
|
||||||
result.extend_from_slice(&iv);
|
let json = serde_json::to_string_pretty(config)
|
||||||
result.extend_from_slice(&buf);
|
.map_err(|e| AppError::Internal(format!("Failed to serialize config: {}", e)))?;
|
||||||
Ok(result)
|
fs::write(&path, json).map_err(|e| AppError::Internal(format!("Failed to write config: {}", e)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrypt_vault(data: &[u8], key: &[u8; 32]) -> AppResult<Vec<u8>> {
|
fn keyring_entry(account: &str) -> AppResult<keyring::Entry> {
|
||||||
if data.len() < IV_LENGTH + 1 {
|
keyring::Entry::new(SERVICE_NAME, account)
|
||||||
return Err(AppError::Encryption("Vault data too short".into()));
|
.map_err(|e| AppError::Keyring(format!("Keyring entry error: {}", e)))
|
||||||
}
|
}
|
||||||
let iv: [u8; 16] = data[..IV_LENGTH].try_into()
|
|
||||||
.map_err(|_| AppError::Encryption("Invalid IV".into()))?;
|
fn keyring_get(account: &str) -> AppResult<Option<String>> {
|
||||||
let mut buf = data[IV_LENGTH..].to_vec();
|
let entry = keyring_entry(account)?;
|
||||||
let cipher = Aes256CbcDec::new(key.into(), &iv.into());
|
match entry.get_password() {
|
||||||
let decrypted = cipher.decrypt_padded_mut::<aes::cipher::block_padding::Pkcs7>(&mut buf)
|
Ok(val) => Ok(Some(val)),
|
||||||
.map_err(|e| AppError::Encryption(format!("Vault decrypt failed: {}", e)))?;
|
Err(keyring::Error::NoEntry) => Ok(None),
|
||||||
Ok(decrypted.to_vec())
|
Err(e) => Err(AppError::Keyring(format!("Keyring read error: {}", e))),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fn read_vault() -> AppResult<SecureVault> {
|
|
||||||
let path = vault_path();
|
fn keyring_set(account: &str, value: &str) -> AppResult<()> {
|
||||||
if !path.exists() {
|
keyring_entry(account)?.set_password(value)
|
||||||
return Ok(SecureVault::default());
|
.map_err(|e| AppError::Keyring(format!("Keyring write error: {}", e)))
|
||||||
}
|
}
|
||||||
let content = fs::read_to_string(&path)
|
|
||||||
.map_err(|e| AppError::Keyring(format!("Failed to read vault: {}", e)))?;
|
fn keyring_delete(account: &str) -> AppResult<()> {
|
||||||
|
let entry = keyring_entry(account)?;
|
||||||
if cfg!(debug_assertions) {
|
match entry.delete_credential() {
|
||||||
return serde_json::from_str::<SecureVault>(&content)
|
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
|
||||||
.map_err(|e| AppError::Keyring(format!("Vault JSON parse error: {}", e)));
|
Err(e) => Err(AppError::Keyring(format!("Keyring delete error: {}", e))),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let raw = BASE64.decode(content.trim())
|
|
||||||
.map_err(|e| AppError::Keyring(format!("Vault corrupted (base64): {}", e)))?;
|
// ===== Public API (signatures unchanged) =====
|
||||||
|
|
||||||
let key = get_vault_key();
|
pub fn get_user_encryption_key(user_id: &str) -> AppResult<String> {
|
||||||
if let Ok(decrypted) = decrypt_vault(&raw, &key) {
|
if cfg!(debug_assertions) {
|
||||||
if let Ok(vault) = serde_json::from_slice::<SecureVault>(&decrypted) {
|
let vault = read_vault()?;
|
||||||
return Ok(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))?
|
||||||
Err(AppError::Keyring("Vault decryption failed — cannot read existing vault".into()))
|
.ok_or_else(|| AppError::Keyring(format!("No encryption key for user {}", user_id)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_vault(vault: &SecureVault) -> AppResult<()> {
|
pub fn set_user_encryption_key(user_id: &str, encryption_key: &str) -> AppResult<()> {
|
||||||
let path = vault_path();
|
if cfg!(debug_assertions) {
|
||||||
if let Some(parent) = path.parent() {
|
let mut vault = read_vault()?;
|
||||||
fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create vault dir: {}", e)))?;
|
vault.encryption_keys.insert(user_id.to_string(), encryption_key.to_string());
|
||||||
}
|
return write_vault(&vault);
|
||||||
|
}
|
||||||
if cfg!(debug_assertions) {
|
keyring_set(&format!("encryption_key:{}", user_id), encryption_key)
|
||||||
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)));
|
pub fn has_user_encryption_key(user_id: &str) -> AppResult<bool> {
|
||||||
}
|
if cfg!(debug_assertions) {
|
||||||
|
let vault = read_vault()?;
|
||||||
let key = get_vault_key();
|
return Ok(vault.encryption_keys.contains_key(user_id));
|
||||||
let json = serde_json::to_string(vault)
|
}
|
||||||
.map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?;
|
Ok(keyring_get(&format!("encryption_key:{}", user_id))?.is_some())
|
||||||
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)))
|
pub fn get_token() -> AppResult<Option<String>> {
|
||||||
}
|
if cfg!(debug_assertions) {
|
||||||
|
let vault = read_vault()?;
|
||||||
// ===== Public API (same signatures as before) =====
|
return Ok(vault.token);
|
||||||
|
}
|
||||||
pub fn get_user_encryption_key(user_id: &str) -> AppResult<String> {
|
keyring_get("token")
|
||||||
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_token(token: &str) -> AppResult<()> {
|
||||||
}
|
if cfg!(debug_assertions) {
|
||||||
|
let mut vault = read_vault()?;
|
||||||
pub fn set_user_encryption_key(user_id: &str, encryption_key: &str) -> AppResult<()> {
|
vault.token = Some(token.to_string());
|
||||||
let mut vault = read_vault()?;
|
return write_vault(&vault);
|
||||||
vault.encryption_keys.insert(user_id.to_string(), encryption_key.to_string());
|
}
|
||||||
write_vault(&vault)
|
keyring_set("token", token)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_user_encryption_key(user_id: &str) -> AppResult<bool> {
|
pub fn remove_token() -> AppResult<()> {
|
||||||
let vault = read_vault()?;
|
if cfg!(debug_assertions) {
|
||||||
Ok(vault.encryption_keys.contains_key(user_id))
|
let mut vault = read_vault()?;
|
||||||
}
|
vault.token = None;
|
||||||
|
return write_vault(&vault);
|
||||||
|
}
|
||||||
pub fn get_token() -> AppResult<Option<String>> {
|
keyring_delete("token")
|
||||||
let vault = read_vault()?;
|
}
|
||||||
Ok(vault.token)
|
|
||||||
}
|
pub fn set_pin_hash(user_id: &str, pin_hash: &str) -> AppResult<()> {
|
||||||
|
if cfg!(debug_assertions) {
|
||||||
pub fn set_token(token: &str) -> AppResult<()> {
|
let mut vault = read_vault()?;
|
||||||
let mut vault = read_vault()?;
|
vault.pin_hashes.insert(user_id.to_string(), pin_hash.to_string());
|
||||||
vault.token = Some(token.to_string());
|
return write_vault(&vault);
|
||||||
write_vault(&vault)
|
}
|
||||||
}
|
keyring_set(&format!("pin_hash:{}", user_id), pin_hash)
|
||||||
|
}
|
||||||
pub fn remove_token() -> AppResult<()> {
|
|
||||||
let mut vault = read_vault()?;
|
pub fn get_pin_hash(user_id: &str) -> AppResult<Option<String>> {
|
||||||
vault.token = None;
|
if cfg!(debug_assertions) {
|
||||||
write_vault(&vault)
|
let vault = read_vault()?;
|
||||||
}
|
return Ok(vault.pin_hashes.get(user_id).cloned());
|
||||||
|
}
|
||||||
pub fn set_pin_hash(user_id: &str, pin_hash: &str) -> AppResult<()> {
|
keyring_get(&format!("pin_hash:{}", user_id))
|
||||||
let mut vault = read_vault()?;
|
}
|
||||||
vault.pin_hashes.insert(user_id.to_string(), pin_hash.to_string());
|
|
||||||
write_vault(&vault)
|
pub fn set_last_user_id(user_id: &str) -> AppResult<()> {
|
||||||
}
|
if cfg!(debug_assertions) {
|
||||||
|
let mut vault = read_vault()?;
|
||||||
pub fn get_pin_hash(user_id: &str) -> AppResult<Option<String>> {
|
vault.last_user_id = Some(user_id.to_string());
|
||||||
let vault = read_vault()?;
|
return write_vault(&vault);
|
||||||
Ok(vault.pin_hashes.get(user_id).cloned())
|
}
|
||||||
}
|
let mut config = read_config()?;
|
||||||
|
config.last_user_id = Some(user_id.to_string());
|
||||||
pub fn set_last_user_id(user_id: &str) -> AppResult<()> {
|
write_config(&config)
|
||||||
let mut vault = read_vault()?;
|
}
|
||||||
vault.last_user_id = Some(user_id.to_string());
|
|
||||||
write_vault(&vault)
|
pub fn get_last_user_id() -> AppResult<Option<String>> {
|
||||||
}
|
if cfg!(debug_assertions) {
|
||||||
|
let vault = read_vault()?;
|
||||||
pub fn get_last_user_id() -> AppResult<Option<String>> {
|
return Ok(vault.last_user_id);
|
||||||
let vault = read_vault()?;
|
}
|
||||||
Ok(vault.last_user_id)
|
let config = read_config()?;
|
||||||
}
|
Ok(config.last_user_id)
|
||||||
|
}
|
||||||
pub fn clear_vault() -> AppResult<()> {
|
|
||||||
write_vault(&SecureVault::default())
|
pub fn clear_vault() -> AppResult<()> {
|
||||||
}
|
if cfg!(debug_assertions) {
|
||||||
|
return write_vault(&SecureVault::default());
|
||||||
const MAX_PIN_ATTEMPTS: i32 = 5;
|
}
|
||||||
const PIN_LOCKOUT_SECONDS: i64 = 300; // 5 minutes
|
// Prod: delete all known keyring entries + reset config
|
||||||
|
keyring_delete("token")?;
|
||||||
pub fn check_pin_rate_limit() -> AppResult<()> {
|
if let Ok(config) = read_config() {
|
||||||
let vault = read_vault()?;
|
if let Some(uid) = &config.last_user_id {
|
||||||
if let Some(locked_until) = vault.pin_locked_until {
|
keyring_delete(&format!("encryption_key:{}", uid))?;
|
||||||
let now = std::time::SystemTime::now()
|
keyring_delete(&format!("pin_hash:{}", uid))?;
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
}
|
||||||
.map(|d| d.as_secs() as i64)
|
}
|
||||||
.unwrap_or(0);
|
write_config(&AppConfig::default())
|
||||||
if now < locked_until {
|
}
|
||||||
let remaining = locked_until - now;
|
|
||||||
return Err(AppError::Auth(format!("Too many attempts. Try again in {} seconds.", remaining)));
|
const MAX_PIN_ATTEMPTS: i32 = 5;
|
||||||
}
|
const PIN_LOCKOUT_SECONDS: i64 = 300;
|
||||||
}
|
|
||||||
Ok(())
|
pub fn check_pin_rate_limit() -> AppResult<()> {
|
||||||
}
|
let (locked_until, _) = if cfg!(debug_assertions) {
|
||||||
|
let vault = read_vault()?;
|
||||||
pub fn record_pin_failure() -> AppResult<()> {
|
(vault.pin_locked_until, vault.pin_failed_attempts)
|
||||||
let mut vault = read_vault()?;
|
} else {
|
||||||
vault.pin_failed_attempts += 1;
|
let config = read_config()?;
|
||||||
if vault.pin_failed_attempts >= MAX_PIN_ATTEMPTS {
|
(config.pin_locked_until, config.pin_failed_attempts)
|
||||||
let now = std::time::SystemTime::now()
|
};
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
if let Some(locked_until) = locked_until {
|
||||||
.map(|d| d.as_secs() as i64)
|
let now = std::time::SystemTime::now()
|
||||||
.unwrap_or(0);
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
vault.pin_locked_until = Some(now + PIN_LOCKOUT_SECONDS);
|
.map(|d| d.as_secs() as i64)
|
||||||
}
|
.unwrap_or(0);
|
||||||
write_vault(&vault)
|
if now < locked_until {
|
||||||
}
|
return Err(AppError::Auth(format!("Too many attempts. Try again in {} seconds.", locked_until - now)));
|
||||||
|
}
|
||||||
pub fn reset_pin_attempts() -> AppResult<()> {
|
}
|
||||||
let mut vault = read_vault()?;
|
Ok(())
|
||||||
vault.pin_failed_attempts = 0;
|
}
|
||||||
vault.pin_locked_until = None;
|
|
||||||
write_vault(&vault)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub fn run() {
|
|||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.plugin(tauri_plugin_http::init())
|
||||||
.manage(db_manager)
|
.manage(db_manager)
|
||||||
.manage(session)
|
.manage(session)
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
|||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
23
vite.config.ts
Normal file
23
vite.config.ts
Normal file
@@ -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',
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user