Remove Act, AutoUpdater, and Book IPC modules alongside associated database logic.

This commit is contained in:
natreex
2026-04-05 19:18:42 -04:00
parent d4765e6576
commit 687c1d582c
99 changed files with 500 additions and 28269 deletions

View File

@@ -58,7 +58,7 @@ export default function LoginForm() {
await tauri.loginSuccess();
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('loginForm.error.server'));
errorMessage(e.message);
} else {
errorMessage(t('loginForm.error.unknown'));
}

View File

@@ -99,7 +99,7 @@ export default function LoginPage() {
await tauri.loginSuccess();
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('loginForm.error.server'));
errorMessage(e.message);
} else {
errorMessage(t('loginForm.error.unknown'));
}

View File

@@ -40,7 +40,7 @@ export default function ForgetPasswordPage() {
}
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('resetPassword.error.emailServer'));
errorMessage(e.message);
} else {
errorMessage(t('resetPassword.error.emailUnknown'));
}
@@ -60,7 +60,7 @@ export default function ForgetPasswordPage() {
}
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('resetPassword.error.codeServer'));
errorMessage(e.message);
} else {
errorMessage(t('resetPassword.error.codeUnknown'));
}
@@ -80,7 +80,7 @@ export default function ForgetPasswordPage() {
}
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('resetPassword.error.passwordServer'));
errorMessage(e.message);
} else {
errorMessage(t('resetPassword.error.passwordUnknown'));
}

View File

@@ -1,3 +1,4 @@
import {fetch} from "@tauri-apps/plugin-http";
import React, {ChangeEvent, useContext, useEffect, useRef, useState} from 'react';
import {
BarChart2,

View File

@@ -3,7 +3,7 @@ import {X} from 'lucide-react';
import IconButton from "@/components/ui/IconButton";
import React, {ChangeEvent, forwardRef, useContext, useImperativeHandle, useState} from "react";
import {apiDelete, apiPost} from "@/lib/api/client";
import axios, {AxiosResponse} from "axios";
import {fetch} from "@tauri-apps/plugin-http";
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import {BookContext, BookContextProps} from "@/context/BookContext";
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
@@ -56,22 +56,17 @@ function BasicInformationSetting(_props: object, ref: React.ForwardedRef<Setting
formData.append('picture', file);
try {
const query: AxiosResponse<ArrayBuffer> = await axios({
method: "POST",
url: configs.apiUrl + `book/cover?bookid=${bookId}`,
headers: {
'Authorization': `Bearer ${userToken}`,
},
params: {
lang: lang,
plateforme: 'web',
},
data: formData,
responseType: 'arraybuffer'
});
const contentType: string = query.headers['content-type'] || 'image/jpeg';
const blob: Blob = new Blob([query.data], {type: contentType});
const query: Response = await fetch(
configs.apiUrl + `book/cover?bookid=${bookId}&lang=${lang}&plateforme=desktop`,
{
method: "POST",
headers: {'Authorization': `Bearer ${userToken}`},
body: formData,
}
);
const contentType: string = query.headers.get('content-type') || 'image/jpeg';
const blob: Blob = new Blob([await query.arrayBuffer()], {type: contentType});
const reader: FileReader = new FileReader();
reader.onloadend = function (): void {

View File

@@ -1,4 +1,5 @@
'use client'
import {fetch} from "@tauri-apps/plugin-http";
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import {Check} from 'lucide-react';
import Button from '@/components/ui/Button';

View File

@@ -108,7 +108,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
setNewIncidentTitle('');
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('errorAddIncident'));
errorMessage(e.message);
} else {
errorMessage(t('errorUnknownAddIncident'));
}
@@ -192,7 +192,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
setSelectedIncidentId('');
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('errorAddPlotPoint'));
errorMessage(e.message);
} else {
errorMessage(t('errorUnknownAddPlotPoint'));
}

View File

@@ -1,3 +1,4 @@
import {fetch} from "@tauri-apps/plugin-http";
import React, {ChangeEvent, useContext, useEffect, useState} from "react";
import {Editor, EditorContent, useEditor} from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";

View File

@@ -95,7 +95,7 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi
localStorage.setItem('userEditorSettings', JSON.stringify(settings));
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('userEditorSettings.saveError'));
errorMessage(e.message);
} else {
errorMessage(t('userEditorSettings.unknownError'));
}

View File

@@ -1,3 +1,4 @@
import {fetch} from "@tauri-apps/plugin-http";
import React, {ChangeEvent, useContext, useState} from 'react';
import {BookOpen, FileInput, Hash, Palette, Wand2} from 'lucide-react';
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
@@ -76,7 +77,7 @@ export default function GhostWriter() {
content = editor?.getText();
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('ghostWriter.errorRetrieveContent'));
errorMessage(e.message);
} else {
errorMessage(t('ghostWriter.errorUnknownRetrieveContent'));
}

View File

@@ -46,7 +46,7 @@ export default function ScribeFooterBar() {
setParagraphCount(paragraphs);
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('errors.wordCountError') + ` (${e.message})`);
errorMessage(e.message);
} else {
errorMessage(t('errors.wordCountError'));
}

View File

@@ -135,7 +135,7 @@ export default function ScribeChapterComponent() {
});
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t("scribeChapterComponent.errorChapterUpdateFr"));
errorMessage(e.message);
} else {
errorMessage(t("scribeChapterComponent.errorChapterUpdateEn"));
}

View File

@@ -56,7 +56,7 @@ export default function InspireMe({hasKey}: { hasKey: boolean }): React.JSX.Elem
content = htmlToText(content);
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(t('inspireMe.error.contentRetrieval'));
errorMessage(e.message);
} else {
errorMessage(t('inspireMe.error.contentRetrievalUnknown'));
}

View File

@@ -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);
}

View File

@@ -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.');
}
};
}

View File

@@ -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(/&apos;/g, "'")
.replace(/&quot;/g, '"')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&#39;/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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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}`);
}

View File

@@ -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;
}
}

View File

@@ -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 = '';
}
}
}

View File

@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
};
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
};
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 '';
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}`

View File

@@ -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`};
}
}

View File

@@ -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
);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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');
}
}

View File

@@ -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
}
]

View File

@@ -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;
}
}

View File

@@ -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
);
}
}

View File

@@ -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
}));
}
}

View File

@@ -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 };
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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
};
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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.");
}
}
}

View File

@@ -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;
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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.");
}
}
}

View File

@@ -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.");
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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.`);
}
}
}

View File

@@ -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.');
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;
}
)
);

View File

@@ -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);
}
)
);

View File

@@ -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);
}
)
);

View File

@@ -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);
}
)
);

View File

@@ -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 };
}
});

View File

@@ -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);
}
));

View File

@@ -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);
}
));

View File

@@ -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);
}
));

View File

@@ -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);
}
));

View File

@@ -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);
}
));

View File

@@ -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);
}
));

View File

@@ -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);
},
),
);

View File

@@ -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;
}
}
})
);

View File

@@ -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);
}
));

View File

@@ -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();
});

View File

@@ -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'),
});

View File

@@ -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
View 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>

View File

@@ -1,104 +1,93 @@
import axios, {AxiosResponse, Method} from "axios";
import {configs, isDesktop} from "@/lib/configs";
type ContentType = 'application/json' | 'multipart/form-data';
interface ApiRequestConfig {
method: Method;
url: string;
auth: string;
lang?: string;
params?: Record<string, unknown>;
data?: unknown;
contentType?: ContentType;
}
export class ApiError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.name = 'ApiError';
}
}
function handleApiError(error: unknown): never {
if (axios.isAxiosError(error)) {
const serverMessage: string = error.response?.data?.message || error.response?.data || error.message;
const statusCode: number = error.response?.status ?? 500;
throw new ApiError(serverMessage, statusCode);
} else if (error instanceof Error) {
throw new Error(error.message);
}
throw new Error('An unexpected error occurred');
}
async function apiRequest<T>(config: ApiRequestConfig): Promise<T> {
try {
const headers: Record<string, string> = {
'Authorization': `Bearer ${config.auth}`
};
if (config.contentType) {
headers['Content-Type'] = config.contentType;
}
const response: AxiosResponse<T> = await axios({
method: config.method,
headers,
params: {
lang: config.lang ?? 'fr',
plateforme: isDesktop ? 'desktop' : 'web',
...config.params
},
url: configs.apiUrl + config.url,
data: config.data
});
return response.data;
} catch (error: unknown) {
handleApiError(error);
}
}
export async function apiGet<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 apiPost<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
return apiRequest<T>({method: 'POST', url, auth, lang, data, contentType: 'application/json'});
}
export async function apiPut<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
return apiRequest<T>({method: 'PUT', url, auth, lang, data, contentType: 'application/json'});
}
export async function apiPatch<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
return apiRequest<T>({method: 'PATCH', url, auth, lang, data, contentType: 'application/json'});
}
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'});
}
export async function apiPostPublic<T>(url: string, data: object, lang: string = "fr"): Promise<T> {
try {
const response: AxiosResponse<T> = await axios({
method: 'POST',
headers: {'Content-Type': 'application/json'},
params: {lang, plateforme: isDesktop ? 'desktop' : 'web'},
url: configs.apiUrl + url,
data
});
return response.data;
} catch (error: unknown) {
handleApiError(error);
}
}
export async function apiUpload<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'});
}
import {fetch} from "@tauri-apps/plugin-http";
import {configs} from "@/lib/configs";
export class ApiError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.name = 'ApiError';
}
}
function buildUrl(url: string, params: Record<string, unknown> = {}, lang: string = "fr"): string {
const fullUrl = new URL(url, configs.apiUrl);
fullUrl.searchParams.set("lang", lang);
fullUrl.searchParams.set("plateforme", "desktop");
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) fullUrl.searchParams.set(key, String(value));
}
return fullUrl.toString();
}
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const body = await response.json().catch(() => ({message: response.statusText}));
throw new ApiError(body.message || body || response.statusText, response.status);
}
return response.json() as Promise<T>;
}
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), {
method: "GET",
headers: {"Authorization": `Bearer ${auth}`},
});
return handleResponse<T>(response);
}
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",
headers: {"Authorization": `Bearer ${auth}`, "Content-Type": "application/json"},
body: JSON.stringify(data),
});
return handleResponse<T>(response);
}
export async function apiPut<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
const response = await fetch(buildUrl(url, {}, lang), {
method: "PUT",
headers: {"Authorization": `Bearer ${auth}`, "Content-Type": "application/json"},
body: JSON.stringify(data),
});
return handleResponse<T>(response);
}
export async function apiPatch<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
const response = await fetch(buildUrl(url, {}, lang), {
method: "PATCH",
headers: {"Authorization": `Bearer ${auth}`, "Content-Type": "application/json"},
body: JSON.stringify(data),
});
return handleResponse<T>(response);
}
export async function apiDelete<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
const response = await fetch(buildUrl(url, {}, lang), {
method: "DELETE",
headers: {"Authorization": `Bearer ${auth}`, "Content-Type": "application/json"},
body: JSON.stringify(data),
});
return handleResponse<T>(response);
}
export async function apiPostPublic<T>(url: string, data: object, lang: string = "fr"): Promise<T> {
const response = await fetch(buildUrl(url, {}, lang), {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(data),
});
return handleResponse<T>(response);
}
export async function apiUpload<T>(url: string, file: File, auth: string, lang: string = "fr"): Promise<T> {
const formData = new FormData();
formData.append("file", file);
const response = await fetch(buildUrl(url, {}, lang), {
method: "POST",
headers: {"Authorization": `Bearer ${auth}`},
body: formData,
});
return handleResponse<T>(response);
}

View File

@@ -8,7 +8,7 @@ export interface Configs {
appVersion: string;
}
const isProduction: boolean = false;
const isProduction: boolean = true;
export const isDesktop: boolean = true;
export const configs: Configs = {

View File

@@ -1,4 +1,4 @@
import axios from 'axios';
import {fetch} from '@tauri-apps/plugin-http';
import {configs, isDesktop} from '@/lib/configs';
interface CrashReportPayload {
@@ -49,9 +49,10 @@ function getUserId(): string | undefined {
async function sendCrashReport(payload: CrashReportPayload): Promise<void> {
try {
await axios.post(configs.apiUrl + 'crash-report', payload, {
await fetch(configs.apiUrl + 'crash-report', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
timeout: 5000,
body: JSON.stringify(payload),
});
} catch {
// Silently fail — we can't crash while reporting a crash

View File

@@ -280,7 +280,6 @@
"emptyDescription": "Search for ideas to enrich your writing. Enter a prompt and let AI inspire you with creative suggestions based on your current content.",
"emptyPromptError": "Please enter a prompt to get inspired.",
"error": {
"contentRetrieval": "Error retrieving content.",
"contentRetrievalUnknown": "Unknown error retrieving content.",
"noBook": "No book selected.",
"noChapter": "No chapter selected.",
@@ -669,7 +668,6 @@
"successSaveAdvanced": "Advanced settings saved successfully.",
"errorSave": "An error occurred during saving.",
"errorUnknownSave": "An unknown error occurred during saving.",
"errorRetrieveContent": "Error retrieving content.",
"errorUnknownRetrieveContent": "Unknown error retrieving content.",
"abortSuccess": "Generation stopped. Token and cost totals will be available on next page refresh.",
"settings": {
@@ -1286,7 +1284,6 @@
"emailLength": "Email address must be between 5 and 100 characters.",
"emailInvalidChars": "Email address contains invalid characters.",
"connection": "Connection error. Check your credentials.",
"server": "Server error. Please try again later.",
"unknown": "An unknown error occurred."
}
},
@@ -1402,11 +1399,8 @@
"error": {
"emailInvalid": "Please enter a valid email address.",
"emailFormat": "The email address format is invalid.",
"emailServer": "Server error while verifying email.",
"emailUnknown": "An unknown error occurred.",
"codeServer": "Server error while verifying code.",
"codeUnknown": "An unknown error occurred.",
"passwordServer": "Server error while changing password.",
"passwordUnknown": "An unknown error occurred."
}
},

View File

@@ -280,7 +280,6 @@
"emptyDescription": "Recherchez des idées pour enrichir votre écriture. Entrez un prompt et laissez l'IA vous inspirer avec des suggestions créatives basées sur votre contenu actuel.",
"emptyPromptError": "Veuillez entrer un prompt pour vous inspirer.",
"error": {
"contentRetrieval": "Erreur lors de la récupération du contenu.",
"contentRetrievalUnknown": "Erreur inconnue lors de la récupération du contenu.",
"noBook": "Aucun livre sélectionné.",
"noChapter": "Aucun chapitre sélectionné.",
@@ -669,7 +668,6 @@
"successSaveAdvanced": "Paramètres avancés sauvegardés avec succès.",
"errorSave": "Une erreur est survenue pendant la sauvegarde.",
"errorUnknownSave": "Une erreur inconnue est survenue pendant la sauvegarde.",
"errorRetrieveContent": "Erreur lors de la récupération du contenu.",
"errorUnknownRetrieveContent": "Erreur inconnue lors de la récupération du contenu.",
"abortSuccess": "Génération arrêtée. Les totaux de tokens et coûts seront disponibles au prochain rafraîchissement de la page.",
"settings": {
@@ -1285,7 +1283,6 @@
"emailLength": "L'adresse e-mail doit contenir entre 5 et 100 caractères.",
"emailInvalidChars": "L'adresse e-mail contient des caractères invalides.",
"connection": "Erreur de connexion. Vérifiez vos identifiants.",
"server": "Erreur du serveur. Veuillez réessayer plus tard.",
"unknown": "Une erreur inconnue est survenue."
}
},
@@ -1401,11 +1398,8 @@
"error": {
"emailInvalid": "Veuillez entrer une adresse e-mail valide.",
"emailFormat": "Le format de l'adresse e-mail est invalide.",
"emailServer": "Erreur du serveur lors de la vérification de l'e-mail.",
"emailUnknown": "Une erreur inconnue est survenue.",
"codeServer": "Erreur du serveur lors de la vérification du code.",
"codeUnknown": "Une erreur inconnue est survenue.",
"passwordServer": "Erreur du serveur lors du changement de mot de passe.",
"passwordUnknown": "Une erreur inconnue est survenue."
}
},

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -3,20 +3,19 @@
"productName": "ERitors Scribe",
"version": "0.5.0",
"type": "module",
"main": "dist/electron/main.js",
"scripts": {
"dev": "vite --port 4000",
"build": "vite build",
"preview": "vite preview",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
"tauri:build": "dotenv -- tauri build",
"tauri:build:win": "PATH=\"/opt/homebrew/opt/llvm/bin:$PATH\" dotenv -- tauri build --runner cargo-xwin --target x86_64-pc-windows-msvc"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@electron/notarize": "^3.1.1",
"@tauri-apps/cli": "^2.10.1",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.10.1",
@@ -25,14 +24,14 @@
"@types/react-dom": "^19.2.3",
"concurrently": "^9.2.1",
"dotenv": "^17.2.3",
"electron": "^39.2.1",
"electron-builder": "^26.0.12",
"dotenv-cli": "^11.0.0",
"typescript": "^5.9.3",
"wait-on": "^9.0.3"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.17",
"@tauri-apps/api": "^2.10.1",
"@tauri-apps/plugin-http": "^2.5.8",
"@tauri-apps/plugin-shell": "^2.3.5",
"@tiptap/extension-color": "^3.10.7",
"@tiptap/extension-gapcursor": "^3.10.7",
@@ -41,17 +40,12 @@
"@tiptap/extension-underline": "^3.10.7",
"@tiptap/react": "^3.10.7",
"@tiptap/starter-kit": "^3.10.7",
"@types/bcrypt": "^6.0.0",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.4.22",
"axios": "^1.13.2",
"bcrypt": "^6.0.0",
"docx": "^9.5.3",
"electron-updater": "^6.7.3",
"i18next": "^25.10.4",
"jszip": "^3.10.1",
"lucide-react": "^0.577.0",
"node-sqlite3-wasm": "^0.8.51",
"pdfkit": "^0.17.2",
"postcss": "^8.5.6",
"react": "^19.2.0",
@@ -60,67 +54,5 @@
"react-router-dom": "^7.13.1",
"tailwindcss": "^4.1.17",
"vite": "^8.0.1"
},
"build": {
"appId": "com.eritors.scribe.desktop",
"productName": "ERitors Scribe",
"buildDependenciesFromSource": false,
"nodeGypRebuild": false,
"npmRebuild": false,
"files": [
"dist/**/*",
"out/**/*",
"build/**/*",
"package.json"
],
"asarUnpack": [
"out/**/*"
],
"directories": {
"output": "release"
},
"mac": {
"icon": "build/icons/mac/icon.icns",
"target": [
"dmg",
"zip"
],
"category": "public.app-category.productivity",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist"
},
"afterSign": "scripts/notarize.cjs",
"win": {
"icon": "build/icons/win/icon.ico",
"target": [
{
"target": "nsis",
"arch": [
"x64",
"ia32"
]
}
]
},
"linux": {
"icon": "build/icons/png",
"target": [
"AppImage",
"deb"
],
"category": "Utility"
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true
},
"publish": {
"provider": "generic",
"url": "https://eritors.com/download/app/desktop"
}
}
}

View File

@@ -1,243 +1,274 @@
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use crate::error::{AppError, AppResult};
const SERVICE_NAME: &str = "com.eritors.scribe.desktop";
const IV_LENGTH: usize = 16;
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
// ===== Secure vault: encrypted JSON file (like Electron's safeStorage) =====
#[derive(Serialize, Deserialize, Default)]
struct SecureVault {
token: Option<String>,
encryption_keys: HashMap<String, String>,
last_user_id: Option<String>,
pin_hashes: HashMap<String, String>,
#[serde(default)]
pin_failed_attempts: i32,
#[serde(default)]
pin_locked_until: Option<i64>,
}
fn vault_path() -> PathBuf {
dirs_next::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(SERVICE_NAME)
.join("secure-config.json")
}
const KEYRING_USER: &str = "vault-key";
/// Retrieves (or generates and stores) the vault encryption key via the OS keyring
/// (macOS Keychain, Windows DPAPI, Linux Secret Service).
/// Falls back to the old derivation method if the keyring is unavailable,
/// and attempts to migrate the key into the keyring for next time.
fn get_vault_key() -> [u8; 32] {
let entry = keyring::Entry::new(SERVICE_NAME, KEYRING_USER);
if let Ok(entry) = &entry {
if let Ok(stored) = entry.get_password() {
if let Ok(decoded) = BASE64.decode(stored.trim()) {
if decoded.len() == 32 {
let mut key = [0u8; 32];
key.copy_from_slice(&decoded);
return key;
}
}
}
}
let mut key = [0u8; 32];
rand::rng().fill_bytes(&mut key);
let encoded = BASE64.encode(key);
if let Ok(entry) = &entry {
let _ = entry.set_password(&encoded);
}
key
}
fn encrypt_vault(data: &[u8], key: &[u8; 32]) -> AppResult<Vec<u8>> {
let mut iv = [0u8; IV_LENGTH];
rand::rng().fill_bytes(&mut iv);
let block_size = 16;
let padding_len = block_size - (data.len() % block_size);
let mut buf = Vec::with_capacity(data.len() + padding_len);
buf.extend_from_slice(data);
buf.extend(std::iter::repeat(padding_len as u8).take(padding_len));
let padded_len = buf.len();
let cipher = Aes256CbcEnc::new(key.into(), &iv.into());
cipher.encrypt_padded_mut::<aes::cipher::block_padding::NoPadding>(&mut buf, padded_len)
.map_err(|e| AppError::Encryption(format!("Vault encrypt failed: {}", e)))?;
let mut result = Vec::with_capacity(IV_LENGTH + buf.len());
result.extend_from_slice(&iv);
result.extend_from_slice(&buf);
Ok(result)
}
fn decrypt_vault(data: &[u8], key: &[u8; 32]) -> AppResult<Vec<u8>> {
if data.len() < IV_LENGTH + 1 {
return Err(AppError::Encryption("Vault data too short".into()));
}
let iv: [u8; 16] = data[..IV_LENGTH].try_into()
.map_err(|_| AppError::Encryption("Invalid IV".into()))?;
let mut buf = data[IV_LENGTH..].to_vec();
let cipher = Aes256CbcDec::new(key.into(), &iv.into());
let decrypted = cipher.decrypt_padded_mut::<aes::cipher::block_padding::Pkcs7>(&mut buf)
.map_err(|e| AppError::Encryption(format!("Vault decrypt failed: {}", e)))?;
Ok(decrypted.to_vec())
}
fn read_vault() -> AppResult<SecureVault> {
let path = vault_path();
if !path.exists() {
return Ok(SecureVault::default());
}
let content = fs::read_to_string(&path)
.map_err(|e| AppError::Keyring(format!("Failed to read vault: {}", e)))?;
if cfg!(debug_assertions) {
return serde_json::from_str::<SecureVault>(&content)
.map_err(|e| AppError::Keyring(format!("Vault JSON parse error: {}", e)));
}
let raw = BASE64.decode(content.trim())
.map_err(|e| AppError::Keyring(format!("Vault corrupted (base64): {}", e)))?;
let key = get_vault_key();
if let Ok(decrypted) = decrypt_vault(&raw, &key) {
if let Ok(vault) = serde_json::from_slice::<SecureVault>(&decrypted) {
return Ok(vault);
}
}
Err(AppError::Keyring("Vault decryption failed — cannot read existing vault".into()))
}
fn write_vault(vault: &SecureVault) -> AppResult<()> {
let path = vault_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create vault dir: {}", e)))?;
}
if cfg!(debug_assertions) {
let json = serde_json::to_string_pretty(vault)
.map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?;
return fs::write(&path, json).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e)));
}
let key = get_vault_key();
let json = serde_json::to_string(vault)
.map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?;
let encrypted = encrypt_vault(json.as_bytes(), &key)?;
let encoded = BASE64.encode(&encrypted);
fs::write(&path, encoded).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e)))
}
// ===== Public API (same signatures as before) =====
pub fn get_user_encryption_key(user_id: &str) -> AppResult<String> {
let vault = read_vault()?;
vault.encryption_keys.get(user_id).cloned()
.ok_or_else(|| AppError::Keyring(format!("No encryption key for user {}", user_id)))
}
pub fn set_user_encryption_key(user_id: &str, encryption_key: &str) -> AppResult<()> {
let mut vault = read_vault()?;
vault.encryption_keys.insert(user_id.to_string(), encryption_key.to_string());
write_vault(&vault)
}
pub fn has_user_encryption_key(user_id: &str) -> AppResult<bool> {
let vault = read_vault()?;
Ok(vault.encryption_keys.contains_key(user_id))
}
pub fn get_token() -> AppResult<Option<String>> {
let vault = read_vault()?;
Ok(vault.token)
}
pub fn set_token(token: &str) -> AppResult<()> {
let mut vault = read_vault()?;
vault.token = Some(token.to_string());
write_vault(&vault)
}
pub fn remove_token() -> AppResult<()> {
let mut vault = read_vault()?;
vault.token = None;
write_vault(&vault)
}
pub fn set_pin_hash(user_id: &str, pin_hash: &str) -> AppResult<()> {
let mut vault = read_vault()?;
vault.pin_hashes.insert(user_id.to_string(), pin_hash.to_string());
write_vault(&vault)
}
pub fn get_pin_hash(user_id: &str) -> AppResult<Option<String>> {
let vault = read_vault()?;
Ok(vault.pin_hashes.get(user_id).cloned())
}
pub fn set_last_user_id(user_id: &str) -> AppResult<()> {
let mut vault = read_vault()?;
vault.last_user_id = Some(user_id.to_string());
write_vault(&vault)
}
pub fn get_last_user_id() -> AppResult<Option<String>> {
let vault = read_vault()?;
Ok(vault.last_user_id)
}
pub fn clear_vault() -> AppResult<()> {
write_vault(&SecureVault::default())
}
const MAX_PIN_ATTEMPTS: i32 = 5;
const PIN_LOCKOUT_SECONDS: i64 = 300; // 5 minutes
pub fn check_pin_rate_limit() -> AppResult<()> {
let vault = read_vault()?;
if let Some(locked_until) = vault.pin_locked_until {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
if now < locked_until {
let remaining = locked_until - now;
return Err(AppError::Auth(format!("Too many attempts. Try again in {} seconds.", remaining)));
}
}
Ok(())
}
pub fn record_pin_failure() -> AppResult<()> {
let mut vault = read_vault()?;
vault.pin_failed_attempts += 1;
if vault.pin_failed_attempts >= MAX_PIN_ATTEMPTS {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
vault.pin_locked_until = Some(now + PIN_LOCKOUT_SECONDS);
}
write_vault(&vault)
}
pub fn reset_pin_attempts() -> AppResult<()> {
let mut vault = read_vault()?;
vault.pin_failed_attempts = 0;
vault.pin_locked_until = None;
write_vault(&vault)
}
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::error::{AppError, AppResult};
const SERVICE_NAME: &str = "com.eritors.scribe.desktop";
// ===== DEV: plain JSON vault (everything in one file, no encryption) =====
#[derive(Serialize, Deserialize, Default)]
struct SecureVault {
token: Option<String>,
encryption_keys: HashMap<String, String>,
last_user_id: Option<String>,
pin_hashes: HashMap<String, String>,
#[serde(default)]
pin_failed_attempts: i32,
#[serde(default)]
pin_locked_until: Option<i64>,
}
// ===== PROD: plain JSON for non-sensitive config only =====
#[derive(Serialize, Deserialize, Default)]
struct AppConfig {
last_user_id: Option<String>,
#[serde(default)]
pin_failed_attempts: i32,
#[serde(default)]
pin_locked_until: Option<i64>,
}
fn config_path() -> PathBuf {
dirs_next::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(SERVICE_NAME)
.join(if cfg!(debug_assertions) { "secure-config.json" } else { "app-config.json" })
}
// ── DEV helpers ──────────────────────────────────────────────────────────
fn read_vault() -> AppResult<SecureVault> {
let path = config_path();
if !path.exists() { return Ok(SecureVault::default()); }
let content = fs::read_to_string(&path)
.map_err(|e| AppError::Keyring(format!("Failed to read vault: {}", e)))?;
serde_json::from_str::<SecureVault>(&content)
.map_err(|e| AppError::Keyring(format!("Vault JSON parse error: {}", e)))
}
fn write_vault(vault: &SecureVault) -> AppResult<()> {
let path = config_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create dir: {}", e)))?;
}
let json = serde_json::to_string_pretty(vault)
.map_err(|e| AppError::Internal(format!("Failed to serialize vault: {}", e)))?;
fs::write(&path, json).map_err(|e| AppError::Internal(format!("Failed to write vault: {}", e)))
}
// ── PROD helpers ─────────────────────────────────────────────────────────
fn read_config() -> AppResult<AppConfig> {
let path = config_path();
if !path.exists() { return Ok(AppConfig::default()); }
let content = fs::read_to_string(&path)
.map_err(|e| AppError::Keyring(format!("Failed to read config: {}", e)))?;
serde_json::from_str::<AppConfig>(&content)
.map_err(|e| AppError::Keyring(format!("Config JSON parse error: {}", e)))
}
fn write_config(config: &AppConfig) -> AppResult<()> {
let path = config_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| AppError::Internal(format!("Failed to create dir: {}", e)))?;
}
let json = serde_json::to_string_pretty(config)
.map_err(|e| AppError::Internal(format!("Failed to serialize config: {}", e)))?;
fs::write(&path, json).map_err(|e| AppError::Internal(format!("Failed to write config: {}", e)))
}
fn keyring_entry(account: &str) -> AppResult<keyring::Entry> {
keyring::Entry::new(SERVICE_NAME, account)
.map_err(|e| AppError::Keyring(format!("Keyring entry error: {}", e)))
}
fn keyring_get(account: &str) -> AppResult<Option<String>> {
let entry = keyring_entry(account)?;
match entry.get_password() {
Ok(val) => Ok(Some(val)),
Err(keyring::Error::NoEntry) => Ok(None),
Err(e) => Err(AppError::Keyring(format!("Keyring read error: {}", e))),
}
}
fn keyring_set(account: &str, value: &str) -> AppResult<()> {
keyring_entry(account)?.set_password(value)
.map_err(|e| AppError::Keyring(format!("Keyring write error: {}", e)))
}
fn keyring_delete(account: &str) -> AppResult<()> {
let entry = keyring_entry(account)?;
match entry.delete_credential() {
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
Err(e) => Err(AppError::Keyring(format!("Keyring delete error: {}", e))),
}
}
// ===== Public API (signatures unchanged) =====
pub fn get_user_encryption_key(user_id: &str) -> AppResult<String> {
if cfg!(debug_assertions) {
let vault = read_vault()?;
return vault.encryption_keys.get(user_id).cloned()
.ok_or_else(|| AppError::Keyring(format!("No encryption key for user {}", user_id)));
}
keyring_get(&format!("encryption_key:{}", user_id))?
.ok_or_else(|| AppError::Keyring(format!("No encryption key for user {}", user_id)))
}
pub fn set_user_encryption_key(user_id: &str, encryption_key: &str) -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.encryption_keys.insert(user_id.to_string(), encryption_key.to_string());
return write_vault(&vault);
}
keyring_set(&format!("encryption_key:{}", user_id), encryption_key)
}
pub fn has_user_encryption_key(user_id: &str) -> AppResult<bool> {
if cfg!(debug_assertions) {
let vault = read_vault()?;
return Ok(vault.encryption_keys.contains_key(user_id));
}
Ok(keyring_get(&format!("encryption_key:{}", user_id))?.is_some())
}
pub fn get_token() -> AppResult<Option<String>> {
if cfg!(debug_assertions) {
let vault = read_vault()?;
return Ok(vault.token);
}
keyring_get("token")
}
pub fn set_token(token: &str) -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.token = Some(token.to_string());
return write_vault(&vault);
}
keyring_set("token", token)
}
pub fn remove_token() -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.token = None;
return write_vault(&vault);
}
keyring_delete("token")
}
pub fn set_pin_hash(user_id: &str, pin_hash: &str) -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.pin_hashes.insert(user_id.to_string(), pin_hash.to_string());
return write_vault(&vault);
}
keyring_set(&format!("pin_hash:{}", user_id), pin_hash)
}
pub fn get_pin_hash(user_id: &str) -> AppResult<Option<String>> {
if cfg!(debug_assertions) {
let vault = read_vault()?;
return Ok(vault.pin_hashes.get(user_id).cloned());
}
keyring_get(&format!("pin_hash:{}", user_id))
}
pub fn set_last_user_id(user_id: &str) -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.last_user_id = Some(user_id.to_string());
return write_vault(&vault);
}
let mut config = read_config()?;
config.last_user_id = Some(user_id.to_string());
write_config(&config)
}
pub fn get_last_user_id() -> AppResult<Option<String>> {
if cfg!(debug_assertions) {
let vault = read_vault()?;
return Ok(vault.last_user_id);
}
let config = read_config()?;
Ok(config.last_user_id)
}
pub fn clear_vault() -> AppResult<()> {
if cfg!(debug_assertions) {
return write_vault(&SecureVault::default());
}
// Prod: delete all known keyring entries + reset config
keyring_delete("token")?;
if let Ok(config) = read_config() {
if let Some(uid) = &config.last_user_id {
keyring_delete(&format!("encryption_key:{}", uid))?;
keyring_delete(&format!("pin_hash:{}", uid))?;
}
}
write_config(&AppConfig::default())
}
const MAX_PIN_ATTEMPTS: i32 = 5;
const PIN_LOCKOUT_SECONDS: i64 = 300;
pub fn check_pin_rate_limit() -> AppResult<()> {
let (locked_until, _) = if cfg!(debug_assertions) {
let vault = read_vault()?;
(vault.pin_locked_until, vault.pin_failed_attempts)
} else {
let config = read_config()?;
(config.pin_locked_until, config.pin_failed_attempts)
};
if let Some(locked_until) = locked_until {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
if now < locked_until {
return Err(AppError::Auth(format!("Too many attempts. Try again in {} seconds.", locked_until - now)));
}
}
Ok(())
}
pub fn record_pin_failure() -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.pin_failed_attempts += 1;
if vault.pin_failed_attempts >= MAX_PIN_ATTEMPTS {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64).unwrap_or(0);
vault.pin_locked_until = Some(now + PIN_LOCKOUT_SECONDS);
}
return write_vault(&vault);
}
let mut config = read_config()?;
config.pin_failed_attempts += 1;
if config.pin_failed_attempts >= MAX_PIN_ATTEMPTS {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64).unwrap_or(0);
config.pin_locked_until = Some(now + PIN_LOCKOUT_SECONDS);
}
write_config(&config)
}
pub fn reset_pin_attempts() -> AppResult<()> {
if cfg!(debug_assertions) {
let mut vault = read_vault()?;
vault.pin_failed_attempts = 0;
vault.pin_locked_until = None;
return write_vault(&vault);
}
let mut config = read_config()?;
config.pin_failed_attempts = 0;
config.pin_locked_until = None;
write_config(&config)
}

View File

@@ -22,6 +22,7 @@ pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_http::init())
.manage(db_manager)
.manage(session)
.invoke_handler(tauri::generate_handler![

View File

@@ -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"
]
}

View File

@@ -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
View 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',
},
});