diff --git a/README.md b/README.md deleted file mode 100644 index b7e625e..0000000 --- a/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# EritorsScribe - Electron + Next.js - -Application Electron avec Next.js et TypeScript. - -## Structure du projet - -``` -eritorsscribe/ -├── electron/ # Code Electron (main process) -│ ├── main.ts # Point d'entrée principal -│ └── preload.ts # Script preload (bridge sécurisé) -├── src/ # Mettez vos fichiers Next.js ici (app/, pages/, components/, etc.) -├── dist/ # Fichiers compilés Electron -├── out/ # Export statique Next.js -├── build/ # Configuration electron-builder -└── release/ # Binaires packagés -``` - -## Installation - -Les dépendances sont déjà installées. Si besoin: - -```bash -npm install -``` - -## Développement - -1. Mettez vos fichiers Next.js dans le dossier `src/` (créez `src/app/` ou `src/pages/` selon votre structure Next.js) - -2. Lancez le mode développement: - -```bash -npm run dev -``` - -Cela va: -- Démarrer Next.js sur http://localhost:3000 -- Lancer Electron qui charge cette URL -- Recharger automatiquement au changement - -## Scripts disponibles - -- `npm run dev` - Développement (Next.js + Electron) -- `npm run dev:next` - Next.js uniquement -- `npm run dev:electron` - Electron uniquement -- `npm run build` - Build complet (Next.js + Electron) -- `npm run start` - Lancer l'app compilée -- `npm run package:mac` - Packager pour macOS -- `npm run package:win` - Packager pour Windows -- `npm run package:linux` - Packager pour Linux -- `npm run package` - Packager pour toutes les plateformes - -## Build de production - -1. Compilez tout: - -```bash -npm run build -``` - -2. Packagez pour votre plateforme: - -```bash -# macOS -npm run package:mac - -# Windows -npm run package:win - -# Linux -npm run package:linux - -# Toutes les plateformes -npm run package -``` - -Les binaires seront dans le dossier `release/`. - -## Versions installées - -- Electron: 39.x (dernière version stable) -- Next.js: 16.x -- React: 19.x -- TypeScript: 5.9.x -- electron-builder: 26.x - -## Configuration Next.js - -Le fichier `next.config.ts` est configuré avec: -- `output: 'export'` - Export statique pour Electron -- `images.unoptimized: true` - Images non optimisées -- `trailingSlash: true` - Compatibilité Electron - -## Sécurité - -Le preload script utilise: -- `contextIsolation: true` -- `nodeIntegration: false` -- `sandbox: true` - -Pour exposer des APIs au renderer, modifiez `electron/preload.ts`. - -## Multi-plateforme - -- macOS: Build sur Mac (requis pour signing) -- Windows: Build sur n'importe quelle plateforme -- Linux: Build sur n'importe quelle plateforme - -## Prochaines étapes - -1. Créez votre structure Next.js dans `src/` -2. Ajoutez vos pages dans `src/app/` ou `src/pages/` -3. Testez avec `npm run dev` -4. Personnalisez `electron/main.ts` selon vos besoins -5. Ajoutez des APIs dans `electron/preload.ts` si nécessaire diff --git a/electron/database/database.service.ts b/electron/database/database.service.ts index cd35704..cb34a33 100644 --- a/electron/database/database.service.ts +++ b/electron/database/database.service.ts @@ -2,31 +2,14 @@ import sqlite3 from 'node-sqlite3-wasm'; import path from 'path'; import { app } from 'electron'; import { initializeSchema } from './schema.js'; -import { encrypt, decrypt, encryptObject, decryptObject, hash } from './encryption.js'; // Type alias for compatibility -type Database = sqlite3.Database; - -// Mappers -import * as BookMapper from './mappers/book.mapper.js'; -import * as ChapterMapper from './mappers/chapter.mapper.js'; -import * as CharacterMapper from './mappers/character.mapper.js'; -import * as AIMapper from './mappers/ai.mapper.js'; -import * as UserMapper from './mappers/user.mapper.js'; -import * as WorldMapper from './mappers/world.mapper.js'; - -// Types from mappers (which contain all necessary interfaces) -import type { BookProps, BookListProps } from './mappers/book.mapper.js'; -import type { ChapterProps } from './mappers/chapter.mapper.js'; -import type { CharacterProps } from './mappers/character.mapper.js'; -import type { Conversation, Message } from './mappers/ai.mapper.js'; -import type { UserProps } from './mappers/user.mapper.js'; -import type { WorldProps } from './mappers/world.mapper.js'; +export type Database = sqlite3.Database; /** - * DatabaseService - Handles all local database operations - * Provides CRUD operations with automatic encryption/decryption - * Maps between DB snake_case and TypeScript camelCase interfaces + * 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; @@ -79,370 +62,25 @@ export class DatabaseService { } /** - * Encrypt sensitive field + * Get database connection + * Use this in repositories and model classes */ - private encryptField(data: string): string { - if (!this.userEncryptionKey) throw new Error('Encryption key not set'); - const encrypted = encrypt(data, this.userEncryptionKey); - return JSON.stringify(encrypted); + getDb(): Database | null { + return this.db; } /** - * Decrypt sensitive field + * Get user encryption key */ - private decryptField(encryptedData: string): string { - if (!this.userEncryptionKey) throw new Error('Encryption key not set'); - try { - const parsed = JSON.parse(encryptedData); - return decrypt(parsed, this.userEncryptionKey); - } catch { - // If not encrypted (for migration), return as-is - return encryptedData; - } - } - - // ========== BOOK OPERATIONS ========== - - /** - * Get all books for the current user - */ - getBooks(): BookListProps[] { - if (!this.db || !this.userId) throw new Error('Database not initialized'); - - const rows = this.db.all(` - SELECT * FROM erit_books - WHERE author_id = ? - ORDER BY book_id DESC - `, [this.userId]) as unknown as BookMapper.DBBook[]; - - return rows.map(row => BookMapper.dbToBookList(row)); + getEncryptionKey(): string | null { + return this.userEncryptionKey; } /** - * Get a single book by ID with all related data + * Get current user ID */ - getBook(bookId: string): BookProps | null { - if (!this.db) throw new Error('Database not initialized'); - - const rows = this.db.all('SELECT * FROM erit_books WHERE book_id = ?', [bookId]) as unknown as BookMapper.DBBook[]; - const row = rows[0]; - - if (!row) return null; - - const book = BookMapper.dbToBook(row); - - // Load chapters - const chapterRows = this.db.all(` - SELECT * FROM book_chapters - WHERE book_id = ? - ORDER BY chapter_order ASC - `, [bookId]) as unknown as ChapterMapper.DBChapter[]; - - book.chapters = chapterRows.map(chapterRow => { - // Load chapter content - const contentRows = this.db!.all(` - SELECT * FROM book_chapter_content - WHERE chapter_id = ? - ORDER BY version DESC - LIMIT 1 - `, [chapterRow.chapter_id]) as unknown as ChapterMapper.DBChapterContent[]; - const contentRow = contentRows[0]; - - // Decrypt content if encrypted - if (contentRow && contentRow.content) { - try { - contentRow.content = this.decryptField(contentRow.content); - } catch (error) { - console.warn('Failed to decrypt chapter content:', error); - } - } - - return ChapterMapper.dbToChapter(chapterRow, contentRow); - }); - - return book; - } - - /** - * Save or update a book - */ - saveBook(book: BookProps | BookListProps, authorId?: string): void { - if (!this.db || !this.userId) throw new Error('Database not initialized'); - - const dbBook = 'bookId' in book - ? BookMapper.bookToDb(book, authorId || this.userId, 0) - : BookMapper.bookListToDb(book, 0); - - // Hash the title - dbBook.hashed_title = hash(dbBook.title); - if (dbBook.sub_title) { - dbBook.hashed_sub_title = hash(dbBook.sub_title); - } - - this.db.run(` - INSERT OR REPLACE 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, words_count, - cover_image, book_meta, synced - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [ - dbBook.book_id, dbBook.type, dbBook.author_id, dbBook.title, dbBook.hashed_title, - dbBook.sub_title ?? null, dbBook.hashed_sub_title, dbBook.summary, dbBook.serie_id ?? null, - dbBook.desired_release_date ?? null, dbBook.desired_word_count ?? null, dbBook.words_count ?? null, - dbBook.cover_image ?? null, dbBook.book_meta ?? null, 0 - ]); - - // Add to pending changes for sync - this.addPendingChange('erit_books', 'INSERT', dbBook.book_id, dbBook); - } - - /** - * Delete a book - */ - deleteBook(bookId: string): void { - if (!this.db) throw new Error('Database not initialized'); - - this.db.run('DELETE FROM erit_books WHERE book_id = ?', [bookId]); - this.addPendingChange('erit_books', 'DELETE', bookId); - } - - // ========== CHAPTER OPERATIONS ========== - - /** - * Save or update a chapter - */ - saveChapter(chapter: ChapterProps, bookId: string, contentId?: string): void { - if (!this.db || !this.userId) throw new Error('Database not initialized'); - - const dbChapter = ChapterMapper.chapterToDb(chapter, bookId, this.userId, 0); - dbChapter.hashed_title = hash(dbChapter.title); - - this.db.run(` - INSERT OR REPLACE INTO book_chapters ( - chapter_id, book_id, author_id, title, hashed_title, words_count, - chapter_order, meta_chapter, synced - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [ - dbChapter.chapter_id, dbChapter.book_id, dbChapter.author_id, dbChapter.title, - dbChapter.hashed_title, dbChapter.words_count ?? null, dbChapter.chapter_order ?? null, - dbChapter.meta_chapter, 0 - ]); - - // Save encrypted content - const dbContent = ChapterMapper.chapterContentToDb( - chapter.chapterContent, - contentId || crypto.randomUUID(), - chapter.chapterId, - this.userId, - 0, - 0 - ); - - // Encrypt the content - dbContent.content = this.encryptField(dbContent.content); - - this.db.run(` - INSERT OR REPLACE INTO book_chapter_content ( - content_id, chapter_id, author_id, version, content, words_count, - meta_chapter_content, time_on_it, synced - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [ - dbContent.content_id, dbContent.chapter_id, dbContent.author_id, dbContent.version, - dbContent.content, dbContent.words_count, dbContent.meta_chapter_content, - dbContent.time_on_it, 0 - ]); - - this.addPendingChange('book_chapters', 'INSERT', chapter.chapterId, dbChapter); - } - - // ========== CHARACTER OPERATIONS ========== - - /** - * Get all characters for a book - */ - getCharacters(bookId: string): CharacterProps[] { - if (!this.db) throw new Error('Database not initialized'); - - const characterRows = this.db.all('SELECT * FROM book_characters WHERE book_id = ?', [bookId]) as unknown as CharacterMapper.DBCharacter[]; - - return characterRows.map(charRow => { - const attrRows = this.db!.all('SELECT * FROM book_characters_attributes WHERE character_id = ?', [charRow.character_id]) as unknown as CharacterMapper.DBCharacterAttribute[]; - return CharacterMapper.dbToCharacter(charRow, attrRows); - }); - } - - /** - * Save or update a character - */ - saveCharacter(character: CharacterProps, bookId: string): void { - if (!this.db || !this.userId) throw new Error('Database not initialized'); - - const dbCharacter = CharacterMapper.characterToDb(character, bookId, this.userId, 0); - - this.db.run(` - INSERT OR REPLACE INTO book_characters ( - character_id, book_id, user_id, first_name, last_name, category, title, - image, role, biography, history, char_meta, synced - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [ - dbCharacter.character_id, dbCharacter.book_id, dbCharacter.user_id, - dbCharacter.first_name, dbCharacter.last_name ?? null, dbCharacter.category, - dbCharacter.title ?? null, dbCharacter.image ?? null, dbCharacter.role ?? null, dbCharacter.biography ?? null, - dbCharacter.history ?? null, dbCharacter.char_meta, 0 - ]); - - // Delete old attributes and insert new ones - this.db.run('DELETE FROM book_characters_attributes WHERE character_id = ?', [dbCharacter.character_id]); - - const attributes = CharacterMapper.characterAttributesToDb(character, this.userId, 0); - - for (const attr of attributes) { - this.db.run(` - INSERT INTO book_characters_attributes ( - attr_id, character_id, user_id, attribute_name, attribute_value, attr_meta, synced - ) VALUES (?, ?, ?, ?, ?, ?, ?) - `, [ - attr.attr_id, attr.character_id, attr.user_id, attr.attribute_name, - attr.attribute_value, attr.attr_meta, 0 - ]); - } - - this.addPendingChange('book_characters', 'INSERT', dbCharacter.character_id, dbCharacter); - } - - // ========== AI CONVERSATION OPERATIONS ========== - - /** - * Get all conversations for a book - */ - getConversations(bookId: string): Conversation[] { - if (!this.db) throw new Error('Database not initialized'); - - const convoRows = this.db.all('SELECT * FROM ai_conversations WHERE book_id = ? ORDER BY start_date DESC', [bookId]) as unknown as AIMapper.DBConversation[]; - - return convoRows.map(convoRow => { - const messageRows = this.db!.all('SELECT * FROM ai_messages_history WHERE conversation_id = ? ORDER BY message_date ASC', [convoRow.conversation_id]) as unknown as AIMapper.DBMessage[]; - - // Decrypt messages - messageRows.forEach(msg => { - try { - msg.message = this.decryptField(msg.message); - } catch (error) { - console.warn('Failed to decrypt AI message:', error); - } - }); - - return AIMapper.dbToConversation(convoRow, messageRows); - }); - } - - /** - * Save a conversation with messages - */ - saveConversation(conversation: Conversation, bookId: string): void { - if (!this.db || !this.userId) throw new Error('Database not initialized'); - - const dbConvo = AIMapper.conversationToDb(conversation, bookId, this.userId, 0); - - this.db.run(` - INSERT OR REPLACE INTO ai_conversations ( - conversation_id, book_id, mode, title, start_date, status, user_id, summary, convo_meta, synced - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [ - dbConvo.conversation_id, dbConvo.book_id, dbConvo.mode, dbConvo.title, - dbConvo.start_date, dbConvo.status, dbConvo.user_id, dbConvo.summary ?? null, - dbConvo.convo_meta, 0 - ]); - - // Save encrypted messages - for (const message of conversation.messages) { - const dbMessage = AIMapper.messageToDb(message, conversation.id, 0); - // Encrypt the message content - dbMessage.message = this.encryptField(dbMessage.message); - - this.db.run(` - INSERT OR REPLACE INTO ai_messages_history ( - message_id, conversation_id, role, message, message_date, meta_message, synced - ) VALUES (?, ?, ?, ?, ?, ?, ?) - `, [ - dbMessage.message_id, dbMessage.conversation_id, dbMessage.role, - dbMessage.message, dbMessage.message_date, dbMessage.meta_message, 0 - ]); - } - - this.addPendingChange('ai_conversations', 'INSERT', dbConvo.conversation_id, dbConvo); - } - - // ========== SYNC OPERATIONS ========== - - /** - * Add a pending change for sync - */ - private addPendingChange(tableName: string, operation: string, recordId: string, data?: any): void { - if (!this.db) return; - - this.db.run(` - INSERT INTO _pending_changes (table_name, operation, record_id, data, created_at) - VALUES (?, ?, ?, ?, ?) - `, [tableName, operation, recordId, data ? JSON.stringify(data) : null, Date.now()]); - - // Update sync metadata - this.db.run(` - UPDATE _sync_metadata - SET pending_changes = pending_changes + 1 - WHERE table_name = ? - `, [tableName]); - } - - /** - * Get pending changes for sync - */ - getPendingChanges(limit: number = 100): any[] { - if (!this.db) throw new Error('Database not initialized'); - - return this.db.all(` - SELECT * FROM _pending_changes - ORDER BY created_at ASC - LIMIT ? - `, [limit]) as any[]; - } - - /** - * Mark changes as synced - */ - markChangesSynced(changeIds: number[]): void { - if (!this.db || changeIds.length === 0) return; - - const placeholders = changeIds.map(() => '?').join(','); - this.db.run(`DELETE FROM _pending_changes WHERE id IN (${placeholders})`, changeIds); - } - - /** - * Update last sync time for a table - */ - updateLastSync(tableName: string): void { - if (!this.db) return; - - this.db.run(` - UPDATE _sync_metadata - SET last_sync_at = ?, pending_changes = 0 - WHERE table_name = ? - `, [Date.now(), tableName]); - } - - /** - * Get sync status - */ - getSyncStatus(): { table: string; lastSync: number; pending: number }[] { - if (!this.db) throw new Error('Database not initialized'); - - const rows = this.db.all('SELECT * FROM _sync_metadata', []) as unknown as any[]; - - return rows.map(row => ({ - table: row.table_name as string, - lastSync: row.last_sync_at as number, - pending: row.pending_changes as number - })); + getUserId(): string | null { + return this.userId; } } diff --git a/electron/database/encryption.ts b/electron/database/encryption.ts index 2f10d3d..968f669 100644 --- a/electron/database/encryption.ts +++ b/electron/database/encryption.ts @@ -1,21 +1,14 @@ import crypto from 'crypto'; /** - * Encryption utilities using AES-256-GCM for local database encryption - * Each user has a unique encryption key derived from their userId and a master secret + * Encryption utilities using AES-256-CBC for local database encryption + * EXACTEMENT comme dans Fastify System.ts */ -const ALGORITHM = 'aes-256-gcm'; +const ALGORITHM = 'aes-256-cbc'; const KEY_LENGTH = 32; // 256 bits const IV_LENGTH = 16; // 128 bits const SALT_LENGTH = 64; -const TAG_LENGTH = 16; - -export interface EncryptedData { - encryptedData: string; - iv: string; - authTag: string; -} /** * Generate a unique encryption key for a user @@ -54,77 +47,34 @@ function extractKeyFromStored(storedKey: string): Buffer { } /** - * Encrypt sensitive data using AES-256-GCM - * @param data - Plain text data to encrypt - * @param userKey - User's encryption key (base64) - * @returns Encrypted data with IV and auth tag - */ -export function encrypt(data: string, userKey: string): EncryptedData { - try { - const key = extractKeyFromStored(userKey); - const iv = crypto.randomBytes(IV_LENGTH); - - const cipher = crypto.createCipheriv(ALGORITHM, key, iv); - - let encrypted = cipher.update(data, 'utf8', 'hex'); - encrypted += cipher.final('hex'); - - const authTag = cipher.getAuthTag(); - - return { - encryptedData: encrypted, - iv: iv.toString('hex'), - authTag: authTag.toString('hex') - }; - } catch (error) { - throw new Error(`Encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - } -} - -/** - * Decrypt data encrypted with AES-256-GCM - * @param encryptedData - Encrypted data object - * @param userKey - User's encryption key (base64) - * @returns Decrypted plain text - */ -export function decrypt(encryptedData: EncryptedData, userKey: string): string { - try { - const key = extractKeyFromStored(userKey); - const iv = Buffer.from(encryptedData.iv, 'hex'); - const authTag = Buffer.from(encryptedData.authTag, 'hex'); - - const decipher = crypto.createDecipheriv(ALGORITHM, key, iv); - decipher.setAuthTag(authTag); - - let decrypted = decipher.update(encryptedData.encryptedData, 'hex', 'utf8'); - decrypted += decipher.final('utf8'); - - return decrypted; - } catch (error) { - throw new Error(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - } -} - -/** - * Encrypt an object by converting it to JSON first - * @param obj - Object to encrypt + * Encrypt data with user key - EXACTEMENT comme Fastify + * @param data - Data to encrypt * @param userKey - User's encryption key - * @returns Encrypted data + * @returns Encrypted string with format "iv:encryptedData" */ -export function encryptObject(obj: T, userKey: string): EncryptedData { - const jsonString = JSON.stringify(obj); - return encrypt(jsonString, userKey); +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 and parse an encrypted object - * @param encryptedData - Encrypted data object + * Decrypt data with user key - EXACTEMENT comme Fastify + * @param encryptedData - Encrypted string with format "iv:encryptedData" * @param userKey - User's encryption key - * @returns Decrypted and parsed object + * @returns Decrypted data */ -export function decryptObject(encryptedData: EncryptedData, userKey: string): T { - const decrypted = decrypt(encryptedData, userKey); - return JSON.parse(decrypted) as T; +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 || ''; } /** @@ -132,6 +82,18 @@ export function decryptObject(encryptedData: EncryptedData, userKey: string): * @param data - Data to hash * @returns Hex encoded hash */ -export function hash(data: string): string { - return crypto.createHash('sha256').update(data).digest('hex'); +export function hashElement(data: string): string { + return crypto.createHash('sha256').update(data.toLowerCase().trim()).digest('hex'); } + +// Pour compatibilité avec l'ancien code +export const encrypt = encryptDataWithUserKey; +export const decrypt = decryptDataWithUserKey; +export const hash = hashElement; + +// Interface pour compatibilité (pas utilisée avec AES-CBC) +export interface EncryptedData { + encryptedData: string; + iv: string; + authTag: string; +} \ No newline at end of file diff --git a/electron/database/mappers/ai.mapper.ts b/electron/database/mappers/ai.mapper.ts deleted file mode 100644 index 0a52b5c..0000000 --- a/electron/database/mappers/ai.mapper.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * TypeScript interfaces (copied from lib/models for type safety) - */ - -export interface Message { - id: number; - type: string; - message: string; - date: string; -} - -export interface Conversation { - id: string; - title?: string; - date?: string; - type?: string; - status: number; - totalPrice: number; - messages: Message[]; -} - -export interface ConversationProps { - id: string; - mode: string; - title: string; - startDate: string; - status: number; -} - -/** - * Database row types (snake_case from SQLite) - */ -export interface DBConversation { - conversation_id: string; - book_id: string; - mode: string; - title: string; - start_date: number; // Unix timestamp - status: number; - user_id: string; - summary?: string; - convo_meta: string; - synced?: number; -} - -export interface DBMessage { - message_id: string; - conversation_id: string; - role: string; // 'user' or 'model' - message: string; - message_date: number; // Unix timestamp - meta_message: string; - synced?: number; -} - -/** - * MAPPERS: DB → TypeScript Interfaces - */ - -export function dbToConversation(dbConvo: DBConversation, messages: DBMessage[] = []): Conversation { - return { - id: dbConvo.conversation_id, - title: dbConvo.title, - date: new Date(dbConvo.start_date).toISOString(), - type: dbConvo.mode as any, - status: dbConvo.status, - totalPrice: 0, // Computed from messages if needed - messages: messages.map(dbToMessage) - }; -} - -export function dbToConversationProps(dbConvo: DBConversation): ConversationProps { - return { - id: dbConvo.conversation_id, - mode: dbConvo.mode, - title: dbConvo.title, - startDate: new Date(dbConvo.start_date).toISOString(), - status: dbConvo.status - }; -} - -export function dbToMessage(dbMessage: DBMessage): Message { - return { - id: parseInt(dbMessage.message_id, 10) || 0, - type: dbMessage.role as any, - message: dbMessage.message, - date: new Date(dbMessage.message_date).toISOString() - }; -} - -/** - * MAPPERS: TypeScript Interfaces → DB - */ - -export function conversationToDb(conversation: Conversation, bookId: string, userId: string, synced: number = 0): DBConversation { - return { - conversation_id: conversation.id, - book_id: bookId, - mode: conversation.type || 'chatbot', - title: conversation.title || 'Untitled Conversation', - start_date: conversation.date ? new Date(conversation.date).getTime() : Date.now(), - status: conversation.status, - user_id: userId, - summary: '', - convo_meta: '', - synced - }; -} - -export function messageToDb(message: Message, conversationId: string, synced: number = 0): DBMessage { - return { - message_id: message.id.toString(), - conversation_id: conversationId, - role: message.type, - message: message.message, - message_date: message.date ? new Date(message.date).getTime() : Date.now(), - meta_message: '', - synced - }; -} diff --git a/electron/database/mappers/book.mapper.ts b/electron/database/mappers/book.mapper.ts deleted file mode 100644 index ae090d1..0000000 --- a/electron/database/mappers/book.mapper.ts +++ /dev/null @@ -1,406 +0,0 @@ -/** - * TypeScript interfaces (copied from lib/models for type safety) - */ - -export interface Author { - id: string; - name: string; - lastName: string; - authorName?: string; -} - -export interface ActChapter { - chapterInfoId: string; - chapterId: string; - title: string; - chapterOrder: number; - actId: number; - incidentId?: string; - plotPointId?: string; - summary: string; - goal: string; -} - -export interface ChapterProps { - chapterId: string; - chapterOrder: number; - title: string; - chapterContent: ChapterContent; -} - -export interface ChapterContent { - version: number; - content: string; - wordsCount: number; -} - -export interface BookProps { - bookId: string; - type: string; - title: string; - author?: Author; - serie?: number; - subTitle?: string; - summary?: string; - publicationDate?: string; - desiredWordCount?: number; - totalWordCount?: number; - coverImage?: string; - chapters?: ChapterProps[]; -} - -export interface BookListProps { - id: string; - type: string; - authorId: string; - title: string; - subTitle?: string; - summary?: string; - serieId?: number; - desiredReleaseDate?: string; - desiredWordCount?: number; - wordCount?: number; - coverImage?: string; - bookMeta?: string; -} - -export interface GuideLine { - 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; - dialogueType: number; - globalResume: string; - atmosphere: string; - verbeTense: number; - langue: number; - themes: string; -} - -export interface PlotPoint { - plotPointId: string; - title: string; - summary: string; - linkedIncidentId: string; - chapters?: ActChapter[]; -} - -export interface Incident { - incidentId: string; - title: string; - summary: string; - chapters?: ActChapter[]; -} - -export interface Issue { - id: string; - name: string; -} - -/** - * Database row types (snake_case from SQLite) - */ -export interface DBBook { - book_id: string; - type: string; - author_id: string; - title: string; - hashed_title: string; - sub_title?: string; - hashed_sub_title: string; - summary: string; - serie_id?: number; - desired_release_date?: string; - desired_word_count?: number; - words_count?: number; - cover_image?: string; - book_meta?: string; - synced?: number; -} - -export interface DBGuideLine { - user_id: string; - book_id: string; - tone: string; - atmosphere: string; - writing_style: string; - themes: string; - symbolism: string; - motifs: string; - narrative_voice: string; - pacing: string; - intended_audience: string; - key_messages: string; - meta_guide_line: string; - synced?: number; -} - -export interface DBGuideLineAI { - user_id: string; - book_id: string; - global_resume: string; - themes: string; - verbe_tense: number; - narrative_type: number; - langue: number; - dialogue_type: number; - tone: string; - atmosphere: string; - current_resume: string; - meta: string; - synced?: number; -} - -export interface DBPlotPoint { - plot_point_id: string; - title: string; - hashed_title: string; - summary?: string; - linked_incident_id?: string; - author_id: string; - book_id: string; - meta_plot: string; - synced?: number; -} - -export interface DBIncident { - incident_id: string; - author_id: string; - book_id: string; - title: string; - hashed_title: string; - summary?: string; - meta_incident: string; - synced?: number; -} - -export interface DBIssue { - issue_id: string; - author_id: string; - book_id: string; - name: string; - hashed_issue_name: string; - meta_issue: string; - synced?: number; -} - -/** - * MAPPERS: DB → TypeScript Interfaces - */ - -export function dbToBookList(dbBook: DBBook): BookListProps { - return { - id: dbBook.book_id, - type: dbBook.type, - authorId: dbBook.author_id, - title: dbBook.title, - subTitle: dbBook.sub_title, - summary: dbBook.summary, - serieId: dbBook.serie_id, - desiredReleaseDate: dbBook.desired_release_date, - desiredWordCount: dbBook.desired_word_count, - wordCount: dbBook.words_count, - coverImage: dbBook.cover_image, - bookMeta: dbBook.book_meta - }; -} - -export function dbToBook(dbBook: DBBook, author?: Author): BookProps { - return { - bookId: dbBook.book_id, - type: dbBook.type, - title: dbBook.title, - author, - serie: dbBook.serie_id, - subTitle: dbBook.sub_title, - summary: dbBook.summary, - publicationDate: dbBook.desired_release_date, - desiredWordCount: dbBook.desired_word_count, - totalWordCount: dbBook.words_count, - coverImage: dbBook.cover_image, - chapters: [] // Populated separately - }; -} - -export function dbToGuideLine(dbGuideLine: DBGuideLine): GuideLine { - return { - tone: dbGuideLine.tone, - atmosphere: dbGuideLine.atmosphere, - writingStyle: dbGuideLine.writing_style, - themes: dbGuideLine.themes, - symbolism: dbGuideLine.symbolism, - motifs: dbGuideLine.motifs, - narrativeVoice: dbGuideLine.narrative_voice, - pacing: dbGuideLine.pacing, - intendedAudience: dbGuideLine.intended_audience, - keyMessages: dbGuideLine.key_messages - }; -} - -export function dbToGuideLineAI(dbGuideLineAI: DBGuideLineAI): GuideLineAI { - return { - narrativeType: dbGuideLineAI.narrative_type, - dialogueType: dbGuideLineAI.dialogue_type, - globalResume: dbGuideLineAI.global_resume, - atmosphere: dbGuideLineAI.atmosphere, - verbeTense: dbGuideLineAI.verbe_tense, - langue: dbGuideLineAI.langue, - themes: dbGuideLineAI.themes - }; -} - -export function dbToPlotPoint(dbPlotPoint: DBPlotPoint): PlotPoint { - return { - plotPointId: dbPlotPoint.plot_point_id, - title: dbPlotPoint.title, - summary: dbPlotPoint.summary || '', - linkedIncidentId: dbPlotPoint.linked_incident_id || '', - chapters: [] // Populated separately - }; -} - -export function dbToIncident(dbIncident: DBIncident): Incident { - return { - incidentId: dbIncident.incident_id, - title: dbIncident.title, - summary: dbIncident.summary || '', - chapters: [] // Populated separately - }; -} - -export function dbToIssue(dbIssue: DBIssue): Issue { - return { - id: dbIssue.issue_id, - name: dbIssue.name - }; -} - -/** - * MAPPERS: TypeScript Interfaces → DB - */ - -export function bookListToDb(book: BookListProps, synced: number = 0): DBBook { - return { - book_id: book.id, - type: book.type, - author_id: book.authorId, - title: book.title, - hashed_title: '', // Will be computed with hash function - sub_title: book.subTitle, - hashed_sub_title: '', - summary: book.summary || '', - serie_id: book.serieId, - desired_release_date: book.desiredReleaseDate, - desired_word_count: book.desiredWordCount, - words_count: book.wordCount, - cover_image: book.coverImage, - book_meta: book.bookMeta || '', - synced - }; -} - -export function bookToDb(book: BookProps, authorId: string, synced: number = 0): DBBook { - return { - book_id: book.bookId, - type: book.type, - author_id: authorId, - title: book.title, - hashed_title: '', - sub_title: book.subTitle, - hashed_sub_title: '', - summary: book.summary || '', - serie_id: book.serie, - desired_release_date: book.publicationDate, - desired_word_count: book.desiredWordCount, - words_count: book.totalWordCount, - cover_image: book.coverImage, - book_meta: '', - synced - }; -} - -export function guideLineToDb(guideLine: GuideLine, userId: string, bookId: string, synced: number = 0): DBGuideLine { - return { - user_id: userId, - book_id: bookId, - tone: guideLine.tone, - atmosphere: guideLine.atmosphere, - writing_style: guideLine.writingStyle, - themes: guideLine.themes, - symbolism: guideLine.symbolism, - motifs: guideLine.motifs, - narrative_voice: guideLine.narrativeVoice, - pacing: guideLine.pacing, - intended_audience: guideLine.intendedAudience, - key_messages: guideLine.keyMessages, - meta_guide_line: '', - synced - }; -} - -export function guideLineAIToDb(guideLineAI: GuideLineAI, userId: string, bookId: string, synced: number = 0): DBGuideLineAI { - return { - user_id: userId, - book_id: bookId, - global_resume: guideLineAI.globalResume, - themes: guideLineAI.themes, - verbe_tense: guideLineAI.verbeTense, - narrative_type: guideLineAI.narrativeType, - langue: guideLineAI.langue, - dialogue_type: guideLineAI.dialogueType, - tone: '', - atmosphere: guideLineAI.atmosphere, - current_resume: '', - meta: '', - synced - }; -} - -export function plotPointToDb(plotPoint: PlotPoint, authorId: string, bookId: string, synced: number = 0): DBPlotPoint { - return { - plot_point_id: plotPoint.plotPointId, - title: plotPoint.title, - hashed_title: '', - summary: plotPoint.summary, - linked_incident_id: plotPoint.linkedIncidentId, - author_id: authorId, - book_id: bookId, - meta_plot: '', - synced - }; -} - -export function incidentToDb(incident: Incident, authorId: string, bookId: string, synced: number = 0): DBIncident { - return { - incident_id: incident.incidentId, - author_id: authorId, - book_id: bookId, - title: incident.title, - hashed_title: '', - summary: incident.summary, - meta_incident: '', - synced - }; -} - -export function issueToDb(issue: Issue, authorId: string, bookId: string, synced: number = 0): DBIssue { - return { - issue_id: issue.id, - author_id: authorId, - book_id: bookId, - name: issue.name, - hashed_issue_name: '', - meta_issue: '', - synced - }; -} diff --git a/electron/database/mappers/chapter.mapper.ts b/electron/database/mappers/chapter.mapper.ts deleted file mode 100644 index bf337ea..0000000 --- a/electron/database/mappers/chapter.mapper.ts +++ /dev/null @@ -1,174 +0,0 @@ -/** - * TypeScript interfaces (copied from lib/models for type safety) - */ - -export interface ChapterContent { - version: number; - content: string; - wordsCount: number; -} - -export interface ChapterProps { - chapterId: string; - chapterOrder: number; - title: string; - chapterContent: ChapterContent; -} - -export interface ActChapter { - chapterInfoId: string; - chapterId: string; - title: string; - chapterOrder: number; - actId: number; - incidentId?: string; - plotPointId?: string; - summary: string; - goal: string; -} - -/** - * Database row types (snake_case from SQLite) - */ -export interface DBChapter { - chapter_id: string; - book_id: string; - author_id: string; - title: string; - hashed_title?: string; - words_count?: number; - chapter_order?: number; - meta_chapter: string; - synced?: number; -} - -export interface DBChapterContent { - content_id: string; - chapter_id: string; - author_id: string; - version: number; - content: string; - words_count: number; - meta_chapter_content: string; - time_on_it: number; - synced?: number; -} - -export interface DBChapterInfo { - chapter_info_id: string; - chapter_id?: string; - act_id?: number; - incident_id?: string; - plot_point_id?: string; - book_id?: string; - author_id?: string; - summary: string; - goal: string; - meta_chapter_info: string; - synced?: number; -} - -/** - * MAPPERS: DB → TypeScript Interfaces - */ - -export function dbToChapter(dbChapter: DBChapter, dbContent?: DBChapterContent): ChapterProps { - const chapterContent: ChapterContent = dbContent ? { - version: dbContent.version, - content: dbContent.content, - wordsCount: dbContent.words_count - } : { - version: 2, - content: '', - wordsCount: 0 - }; - - return { - chapterId: dbChapter.chapter_id, - chapterOrder: dbChapter.chapter_order || 0, - title: dbChapter.title, - chapterContent - }; -} - -export function dbToChapterContent(dbContent: DBChapterContent): ChapterContent { - return { - version: dbContent.version, - content: dbContent.content, - wordsCount: dbContent.words_count - }; -} - -export function dbToActChapter(dbChapter: DBChapter, dbInfo: DBChapterInfo): ActChapter { - return { - chapterInfoId: dbInfo.chapter_info_id, - chapterId: dbChapter.chapter_id, - title: dbChapter.title, - chapterOrder: dbChapter.chapter_order || 0, - actId: dbInfo.act_id || 0, - incidentId: dbInfo.incident_id, - plotPointId: dbInfo.plot_point_id, - summary: dbInfo.summary, - goal: dbInfo.goal - }; -} - -/** - * MAPPERS: TypeScript Interfaces → DB - */ - -export function chapterToDb(chapter: ChapterProps, bookId: string, authorId: string, synced: number = 0): DBChapter { - return { - chapter_id: chapter.chapterId, - book_id: bookId, - author_id: authorId, - title: chapter.title, - hashed_title: '', - words_count: chapter.chapterContent.wordsCount, - chapter_order: chapter.chapterOrder, - meta_chapter: '', - synced - }; -} - -export function chapterContentToDb( - content: ChapterContent, - contentId: string, - chapterId: string, - authorId: string, - timeOnIt: number = 0, - synced: number = 0 -): DBChapterContent { - return { - content_id: contentId, - chapter_id: chapterId, - author_id: authorId, - version: content.version, - content: content.content, - words_count: content.wordsCount, - meta_chapter_content: '', - time_on_it: timeOnIt, - synced - }; -} - -export function actChapterToDbInfo( - actChapter: ActChapter, - bookId: string, - authorId: string, - synced: number = 0 -): DBChapterInfo { - return { - chapter_info_id: actChapter.chapterInfoId, - chapter_id: actChapter.chapterId, - act_id: actChapter.actId, - incident_id: actChapter.incidentId, - plot_point_id: actChapter.plotPointId, - book_id: bookId, - author_id: authorId, - summary: actChapter.summary, - goal: actChapter.goal, - meta_chapter_info: '', - synced - }; -} diff --git a/electron/database/mappers/character.mapper.ts b/electron/database/mappers/character.mapper.ts deleted file mode 100644 index 85adb06..0000000 --- a/electron/database/mappers/character.mapper.ts +++ /dev/null @@ -1,185 +0,0 @@ -/** - * TypeScript interfaces (copied from lib/models for type safety) - */ - -export interface Attribute { - id?: string; - name: string; - description: string; -} - -export interface CharacterProps { - id: string | null; - name: string; - lastName: string; - category: string; - title: string; - image: string; - physical?: Attribute[]; - psychological?: Attribute[]; - relations?: Attribute[]; - skills?: Attribute[]; - weaknesses?: Attribute[]; - strengths?: Attribute[]; - goals?: Attribute[]; - motivations?: Attribute[]; - role: string; - biography?: string; - history?: string; -} - -/** - * Database row types (snake_case from SQLite) - */ -export interface DBCharacter { - character_id: string; - book_id: string; - user_id: string; - first_name: string; - last_name?: string; - category: string; - title?: string; - image?: string; - role?: string; - biography?: string; - history?: string; - char_meta: string; - synced?: number; -} - -export interface DBCharacterAttribute { - attr_id: string; - character_id: string; - user_id: string; - attribute_name: string; // Format: "section:attributeName" (e.g., "physical:Height") - attribute_value: string; // JSON stringified Attribute - attr_meta: string; - synced?: number; -} - -/** - * MAPPERS: DB → TypeScript Interfaces - */ - -export function dbToCharacter(dbChar: DBCharacter, attributes: DBCharacterAttribute[] = []): CharacterProps { - // Group attributes by section - const physical: Attribute[] = []; - const psychological: Attribute[] = []; - const relations: Attribute[] = []; - const skills: Attribute[] = []; - const weaknesses: Attribute[] = []; - const strengths: Attribute[] = []; - const goals: Attribute[] = []; - const motivations: Attribute[] = []; - - for (const attr of attributes) { - try { - const parsedValue: Attribute = JSON.parse(attr.attribute_value); - const section = attr.attribute_name.split(':')[0]; - - switch (section) { - case 'physical': - physical.push(parsedValue); - break; - case 'psychological': - psychological.push(parsedValue); - break; - case 'relations': - relations.push(parsedValue); - break; - case 'skills': - skills.push(parsedValue); - break; - case 'weaknesses': - weaknesses.push(parsedValue); - break; - case 'strengths': - strengths.push(parsedValue); - break; - case 'goals': - goals.push(parsedValue); - break; - case 'motivations': - motivations.push(parsedValue); - break; - } - } catch (error) { - console.error('Failed to parse character attribute:', error); - } - } - - return { - id: dbChar.character_id, - name: dbChar.first_name, - lastName: dbChar.last_name || '', - category: dbChar.category as any, - title: dbChar.title || '', - image: dbChar.image || '', - physical, - psychological, - relations, - skills, - weaknesses, - strengths, - goals, - motivations, - role: dbChar.role || '', - biography: dbChar.biography, - history: dbChar.history - }; -} - -/** - * MAPPERS: TypeScript Interfaces → DB - */ - -export function characterToDb(character: CharacterProps, bookId: string, userId: string, synced: number = 0): DBCharacter { - return { - character_id: character.id || crypto.randomUUID(), - book_id: bookId, - user_id: userId, - first_name: character.name, - last_name: character.lastName, - category: character.category, - title: character.title, - image: character.image, - role: character.role, - biography: character.biography, - history: character.history, - char_meta: '', - synced - }; -} - -export function characterAttributesToDb( - character: CharacterProps, - userId: string, - synced: number = 0 -): DBCharacterAttribute[] { - const attributes: DBCharacterAttribute[] = []; - - const addAttributes = (section: string, attrs: Attribute[]) => { - for (const attr of attrs) { - attributes.push({ - attr_id: attr.id || crypto.randomUUID(), - character_id: character.id || '', - user_id: userId, - attribute_name: `${section}:${attr.name}`, - attribute_value: JSON.stringify(attr), - attr_meta: '', - synced - }); - } - }; - - addAttributes('physical', character.physical || []); - addAttributes('psychological', character.psychological || []); - addAttributes('relations', character.relations || []); - addAttributes('skills', character.skills || []); - addAttributes('weaknesses', character.weaknesses || []); - addAttributes('strengths', character.strengths || []); - addAttributes('goals', character.goals || []); - addAttributes('motivations', character.motivations || []); - - return attributes; -} diff --git a/electron/database/mappers/user.mapper.ts b/electron/database/mappers/user.mapper.ts deleted file mode 100644 index 42004b0..0000000 --- a/electron/database/mappers/user.mapper.ts +++ /dev/null @@ -1,153 +0,0 @@ -/** - * TypeScript interfaces (copied from lib/models for type safety) - */ - -export interface Subscription { - subType: string; - subTier: number; - status: boolean; -} - -export interface UserProps { - id: string; - name: string; - lastName: string; - username: string; - authorName?: string; - email?: string; - accountVerified: boolean; - termsAccepted: boolean; - aiUsage: number; - apiKeys: { - gemini: boolean; - openai: boolean; - anthropic: boolean; - }; - books?: any[]; - guideTour?: { [key: string]: boolean }[]; - subscription?: Subscription[]; - writingLang: number; - writingLevel: number; - ritePoints: number; - creditsBalance: number; - groupId: number; -} - -/** - * Database row types (snake_case from SQLite) - */ -export interface DBUser { - user_id: string; - first_name: string; - last_name: string; - username: string; - email: string; - origin_email: string; - origin_username: string; - author_name?: string; - origin_author_name?: string; - plateform: string; - social_id?: string; - user_group: number; - password?: string; - term_accepted: number; - verify_code?: string; - reg_date: number; - account_verified: number; - user_meta: string; // JSON containing apiKeys, guideTour, writingLang, writingLevel, aiUsage - erite_points: number; - stripe_customer_id?: string; - credits_balance: number; - synced?: number; -} - -interface UserMeta { - apiKeys?: { - gemini: boolean; - openai: boolean; - anthropic: boolean; - }; - guideTour?: { [key: string]: boolean }[]; - subscription?: Subscription[]; - writingLang?: number; - writingLevel?: number; - aiUsage?: number; -} - -/** - * MAPPERS: DB → TypeScript Interfaces - */ - -export function dbToUser(dbUser: DBUser): UserProps { - let meta: UserMeta = {}; - try { - meta = JSON.parse(dbUser.user_meta || '{}'); - } catch (error) { - console.error('Failed to parse user_meta:', error); - } - - return { - id: dbUser.user_id, - name: dbUser.first_name, - lastName: dbUser.last_name, - username: dbUser.username, - authorName: dbUser.author_name, - email: dbUser.email, - accountVerified: dbUser.account_verified === 1, - termsAccepted: dbUser.term_accepted === 1, - aiUsage: meta.aiUsage || 0, - apiKeys: meta.apiKeys || { - gemini: false, - openai: false, - anthropic: false - }, - books: [], // Populated separately - guideTour: meta.guideTour || [], - subscription: meta.subscription || [], - writingLang: meta.writingLang || 1, - writingLevel: meta.writingLevel || 1, - ritePoints: dbUser.erite_points, - creditsBalance: dbUser.credits_balance, - groupId: dbUser.user_group - }; -} - -/** - * MAPPERS: TypeScript Interfaces → DB - */ - -export function userToDb(user: UserProps, synced: number = 0): DBUser { - const meta: UserMeta = { - apiKeys: user.apiKeys, - guideTour: user.guideTour, - subscription: user.subscription, - writingLang: user.writingLang, - writingLevel: user.writingLevel, - aiUsage: user.aiUsage - }; - - return { - user_id: user.id, - first_name: user.name, - last_name: user.lastName, - username: user.username, - email: user.email || '', - origin_email: user.email || '', - origin_username: user.username, - author_name: user.authorName, - origin_author_name: user.authorName, - plateform: 'electron', - social_id: undefined, - user_group: user.groupId, - password: undefined, - term_accepted: user.termsAccepted ? 1 : 0, - verify_code: undefined, - reg_date: Date.now(), - account_verified: user.accountVerified ? 1 : 0, - user_meta: JSON.stringify(meta), - erite_points: user.ritePoints, - stripe_customer_id: undefined, - credits_balance: user.creditsBalance, - synced - }; -} diff --git a/electron/database/mappers/world.mapper.ts b/electron/database/mappers/world.mapper.ts deleted file mode 100644 index 5002dcb..0000000 --- a/electron/database/mappers/world.mapper.ts +++ /dev/null @@ -1,222 +0,0 @@ -/** - * TypeScript interfaces (copied from lib/models for type safety) - */ - -export interface WorldElement { - id: string; - name: string; - description: string; -} - -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[]; -} - -/** - * Database row types (snake_case from SQLite) - */ -export interface DBWorld { - world_id: string; - name: string; - hashed_name: string; - author_id: string; - book_id: string; - history?: string; - politics?: string; - economy?: string; - religion?: string; - languages?: string; - meta_world: string; - synced?: number; -} - -export interface DBWorldElement { - element_id: string; - world_id: string; - user_id: string; - element_type: number; // Type identifier for different element categories - name: string; - original_name: string; - description?: string; - meta_element: string; - synced?: number; -} - -// Element type constants -export enum WorldElementType { - LAW = 1, - BIOME = 2, - ISSUE = 3, - CUSTOM = 4, - KINGDOM = 5, - CLIMATE = 6, - RESOURCE = 7, - WILDLIFE = 8, - ART = 9, - ETHNIC_GROUP = 10, - SOCIAL_CLASS = 11, - IMPORTANT_CHARACTER = 12 -} - -/** - * MAPPERS: DB → TypeScript Interfaces - */ - -export function dbToWorld(dbWorld: DBWorld, elements: DBWorldElement[] = []): WorldProps { - // Group elements by type - const laws: WorldElement[] = []; - const biomes: WorldElement[] = []; - const issues: WorldElement[] = []; - const customs: WorldElement[] = []; - const kingdoms: WorldElement[] = []; - const climate: WorldElement[] = []; - const resources: WorldElement[] = []; - const wildlife: WorldElement[] = []; - const arts: WorldElement[] = []; - const ethnicGroups: WorldElement[] = []; - const socialClasses: WorldElement[] = []; - const importantCharacters: WorldElement[] = []; - - for (const elem of elements) { - const worldElement: WorldElement = { - id: elem.element_id, - name: elem.name, - description: elem.description || '' - }; - - switch (elem.element_type) { - case WorldElementType.LAW: - laws.push(worldElement); - break; - case WorldElementType.BIOME: - biomes.push(worldElement); - break; - case WorldElementType.ISSUE: - issues.push(worldElement); - break; - case WorldElementType.CUSTOM: - customs.push(worldElement); - break; - case WorldElementType.KINGDOM: - kingdoms.push(worldElement); - break; - case WorldElementType.CLIMATE: - climate.push(worldElement); - break; - case WorldElementType.RESOURCE: - resources.push(worldElement); - break; - case WorldElementType.WILDLIFE: - wildlife.push(worldElement); - break; - case WorldElementType.ART: - arts.push(worldElement); - break; - case WorldElementType.ETHNIC_GROUP: - ethnicGroups.push(worldElement); - break; - case WorldElementType.SOCIAL_CLASS: - socialClasses.push(worldElement); - break; - case WorldElementType.IMPORTANT_CHARACTER: - importantCharacters.push(worldElement); - break; - } - } - - return { - id: dbWorld.world_id, - name: dbWorld.name, - history: dbWorld.history || '', - politics: dbWorld.politics || '', - economy: dbWorld.economy || '', - religion: dbWorld.religion || '', - languages: dbWorld.languages || '', - laws, - biomes, - issues, - customs, - kingdoms, - climate, - resources, - wildlife, - arts, - ethnicGroups, - socialClasses, - importantCharacters - }; -} - -/** - * MAPPERS: TypeScript Interfaces → DB - */ - -export function worldToDb(world: WorldProps, authorId: string, bookId: string, synced: number = 0): DBWorld { - return { - world_id: world.id, - name: world.name, - hashed_name: '', - author_id: authorId, - book_id: bookId, - history: world.history, - politics: world.politics, - economy: world.economy, - religion: world.religion, - languages: world.languages, - meta_world: '', - synced - }; -} - -export function worldElementsToDb(world: WorldProps, userId: string, synced: number = 0): DBWorldElement[] { - const elements: DBWorldElement[] = []; - - const addElements = (type: WorldElementType, elems: WorldElement[]) => { - for (const elem of elems) { - elements.push({ - element_id: elem.id, - world_id: world.id, - user_id: userId, - element_type: type, - name: elem.name, - original_name: elem.name, - description: elem.description, - meta_element: '', - synced - }); - } - }; - - addElements(WorldElementType.LAW, world.laws || []); - addElements(WorldElementType.BIOME, world.biomes || []); - addElements(WorldElementType.ISSUE, world.issues || []); - addElements(WorldElementType.CUSTOM, world.customs || []); - addElements(WorldElementType.KINGDOM, world.kingdoms || []); - addElements(WorldElementType.CLIMATE, world.climate || []); - addElements(WorldElementType.RESOURCE, world.resources || []); - addElements(WorldElementType.WILDLIFE, world.wildlife || []); - addElements(WorldElementType.ART, world.arts || []); - addElements(WorldElementType.ETHNIC_GROUP, world.ethnicGroups || []); - addElements(WorldElementType.SOCIAL_CLASS, world.socialClasses || []); - addElements(WorldElementType.IMPORTANT_CHARACTER, world.importantCharacters || []); - - return elements; -} diff --git a/electron/database/repositories/book.repository.ts b/electron/database/repositories/book.repository.ts new file mode 100644 index 0000000..2c37a5d --- /dev/null +++ b/electron/database/repositories/book.repository.ts @@ -0,0 +1,728 @@ +import {Database, QueryResult, RunResult, SQLiteValue} from 'node-sqlite3-wasm'; +import System from "@/electron/database/System"; + +export interface BookQuery extends Record { + book_id: string; + type: string; + author_id: string; + title: string; + hashed_title: string; + sub_title: string | null; + hashed_sub_title: string | null; + summary: string | null; + serie_id: number | null; + desired_release_date: string | null; + desired_word_count: number | null; + words_count: number | null; + cover_image: string | null; + book_meta: string | null; +} + +export interface GuideLineQuery extends Record { + tone: string; + atmosphere: string; + writing_style: string; + themes: string; + symbolism: string; + motifs: string; + narrative_voice: string; + pacing: string; + intended_audience: string; + key_messages: string; + meta_guide_line: string; +} + +export interface PlotPointQuery extends Record { + plot_point_id: string; + title: string; + summary: string; + linked_incident_id: string | null; + meta_plot: string; +} + +export interface IncidentQuery extends Record { + incident_id: string; + title: string; + summary: string; + meta_incident: string; +} + +export interface IssueQuery extends Record { + issue_id: string; + name: string; + meta_issue: string; +} + +export interface ActQuery extends Record { + act_index: number; + summary: string; + meta_acts: string; +} + +export interface MetaBookQuery extends Record { + book_meta: string; +} + +export interface BookCoverQuery extends Record { + cover_image: string; + book_meta: string; +} + +export interface ChapterBookResult extends Record { + title: string; + chapter_order: number; + meta_chapter: string; + content: string | null; + meta_chapter_content: string | null; +} + +export interface WorldQuery extends Record { + world_id: string; + world_name: string; + history: string | null; + politics: string | null; + economy: string | null; + religion: string | null; + languages: string | null; + meta_world: string; + element_id: string | null; + element_name: string | null; + element_description: string | null; + element_type: number | null; + meta_element: string | null; +} + +export interface WorldElementValue { + id: string; + name: string; + description: string; + type: number; + meta: string; +} + +export interface GuideLineAIQuery extends Record { + user_id: string; + book_id: string; + global_resume: string | null; + themes: string | null; + verbe_tense: number | null; + narrative_type: number | null; + langue: number | null; + dialogue_type: number | null; + tone: string | null; + atmosphere: string | null; + current_resume: string | null; + meta: string; +} + +export default class BookRepo { + public static fetchBooks(userId: string, lang: 'fr' | 'en'): BookQuery[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT book_id,type,author_id,`title`,`sub_title`,summary,serie_id,desired_release_date,desired_word_count,words_count,cover_image,`book_meta` FROM erit_books WHERE author_id = ? ORDER BY book_id DESC', [userId]) 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.'); + } else { + console.error(error); + throw new Error(lang === 'fr' ? 'Une erreur inconnue est survenue.' : 'An unknown error occurred.'); + } + } + } + + public static updateBookCover(bookId:string,coverImageName:string,userId:string, lang: 'fr' | 'en'):boolean{ + try { + const db: Database = System.getDb(); + const result:RunResult = db.run('UPDATE `erit_books` SET cover_image=? WHERE `book_id`=? AND author_id=?', [coverImageName, bookId, userId]); + return result.changes>0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour la couverture du livre.` : `Unable to update book cover.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + public static fetchBook(bookId: string, userId: string, lang: 'fr' | 'en'): BookQuery { + let result: BookQuery; + try { + const db: Database = System.getDb(); + result = db.get('SELECT book_id, author_id, `title`, `summary`, `sub_title`, `cover_image`,`desired_release_date`, desired_word_count, `words_count`, book_meta FROM `erit_books` WHERE `book_id`=? AND author_id=?', [bookId, userId]) 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.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (!result) { + throw new Error(lang === 'fr' ? `Livre non trouvé.` : `Book not found.`); + } + return result; + } + public static verifyBookExist(hashedTitle:string,hashedSubTitle:string,userId:string, lang: 'fr' | 'en'):boolean{ + try { + const db: Database = System.getDb(); + const result:QueryResult|null = db.get('SELECT book_id FROM erit_books WHERE hashed_title=? AND author_id=? AND erit_books.hashed_sub_title=?', [hashedTitle,userId,hashedSubTitle]); + return result!==null; + } catch (err: unknown) { + if (err instanceof Error) { + console.error(`DB Error: ${err.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du livre.` : `Unable to verify book existence.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + public static fetchGuideLine(userId: string, bookId: string, lang: 'fr' | 'en'): GuideLineQuery[] { + let result: GuideLineQuery[]; + try { + const db: Database = System.getDb(); + result = db.all('SELECT * FROM book_guide_line WHERE book_id=? AND user_id=?', [bookId, userId]) as GuideLineQuery[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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 result; + } + + public static fetchAllActs(userId: string, bookId: string, lang: 'fr' | 'en'): ActQuery[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT `act_index`,`summary`,`meta_acts` FROM `book_act_summaries` WHERE `book_id`=? AND `user_id`=?', [bookId, userId]) as ActQuery[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + public static fetchAllIncitentIncidents(userId:string,bookId:string, lang: 'fr' | 'en'):IncidentQuery[]{ + try { + const db: Database = System.getDb(); + return db.all('SELECT incident_id,`title`,`summary`,`meta_incident` FROM `book_incidents` WHERE `author_id`=? AND `book_id`=?', [userId, bookId]) as IncidentQuery[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + public static fetchAllPlotPoints(userId:string,bookId:string, lang: 'fr' | 'en'):PlotPointQuery[]{ + try { + const db: Database = System.getDb(); + return db.all('SELECT plot_point_id,`title`,`summary`,`linked_incident_id`,`meta_plot` FROM `book_plot_points` WHERE `author_id`=? AND `book_id`=?', [userId, bookId]) as PlotPointQuery[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + public static fetchIssuesFromBook(userId:string,bookId:string, lang: 'fr' | 'en'):IssueQuery[]{ + try { + const db: Database = System.getDb(); + return db.all('SELECT issue_id,`name`,`meta_issue` FROM `book_issues` WHERE `author_id`=? AND `book_id`=?', [userId, bookId]) as IssueQuery[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + 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 result:RunResult + try { + const db: Database = System.getDb(); + result = db.run('INSERT INTO erit_books (book_id,type,author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, erit_books.desired_word_count) VALUES (?,?,?,?,?,?,?,?,?,?,?)', [bookId, type, userId, encryptedTitle, hashedTitle, encryptedSubTitle, hashedSubTitle, encryptedSummary, serie, publicationDate ? publicationDate : null, desiredWordCount]); + } catch (err: unknown) { + if (err instanceof Error) { + console.error(`DB Error: ${err.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter le livre.` : `Unable to add book.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (result.changes > 0) { + return bookId; + } else { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du livre.` : `Error adding book.`); + } + } + public static fetchBookCover(userId:string,bookId:string, lang: 'fr' | 'en'):BookCoverQuery{ + try { + const db: Database = System.getDb(); + return db.get('SELECT cover_image, book_meta FROM erit_books WHERE author_id=? AND book_id=?', [userId, bookId]) as BookCoverQuery; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer la couverture du livre.` : `Unable to retrieve book cover.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static updateBookBasicInformation(userId: string, title: string, hashedTitle: string, subTitle: string, hashedSubTitle: string, summary: string, publicationDate: string, wordCount: number, bookId: string, bookMeta: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('UPDATE erit_books SET title=?, hashed_title=?, sub_title=?, hashed_sub_title=?, summary=?, serie_id=?, desired_release_date=?, desired_word_count=?, book_meta=? WHERE author_id=? AND book_id=?', + [title, hashedTitle, subTitle, hashedSubTitle, summary, 0, publicationDate ? System.dateToMySqlDate(publicationDate) : null, wordCount, bookMeta, userId, bookId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour les informations du livre.` : `Unable to update book information.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static updateGuideLine(userId: string, bookId: string, encryptedTone: string, encryptedAtmosphere: string, encryptedWritingStyle: string, encryptedThemes: string, encryptedSymbolism: string, encryptedMotifs: string, encryptedNarrativeVoice: string, encryptedPacing: string, encryptedKeyMessages: string, encryptedIntendedAudience: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('UPDATE book_guide_line SET tone=?, atmosphere=?, writing_style=?, themes=?, symbolism=?, motifs=?, narrative_voice=?, pacing=?, key_messages=? WHERE user_id=? AND book_id=?', [encryptedTone, encryptedAtmosphere, encryptedWritingStyle, encryptedThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedKeyMessages, userId, bookId]); + if (result.changes > 0) { + return true; + } else { + const insert:RunResult = db.run('INSERT INTO book_guide_line (user_id, book_id, tone, atmosphere, writing_style, themes, symbolism, motifs, narrative_voice, pacing, intended_audience, key_messages) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', [userId, bookId, encryptedTone, encryptedAtmosphere, encryptedWritingStyle, encryptedThemes, encryptedSymbolism, encryptedMotifs, encryptedNarrativeVoice, encryptedPacing, encryptedIntendedAudience, encryptedKeyMessages]); + return insert.changes > 0; + } + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static updateActSummary(userId: string, bookId: string, actId: number, summary: string, metaActs: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('UPDATE book_act_summaries SET summary=?, meta_acts=? WHERE user_id=? AND book_id=? AND act_sum_id=?', [summary, metaActs, userId, bookId, actId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static insertNewIncident(incidentId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, lang: 'fr' | 'en'): string { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('INSERT INTO book_incidents (incident_id,author_id, book_id, title, hashed_title) VALUES (?,?,?,?,?)', [incidentId, userId, bookId, encryptedName, hashedName]); + if (result.changes > 0) { + return incidentId; + } else { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout de l'élément déclencheur.` : `Error adding incident.`); + } + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static deleteIncident(userId: string, bookId: string, incidentId: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('DELETE FROM book_incidents WHERE author_id=? AND book_id=? AND incident_id=?', [userId, bookId, incidentId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + static insertNewPlotPoint(plotPointId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, incidentId: string, metaPlot: string, lang: 'fr' | 'en'): string { + try { + const db: Database = System.getDb(); + const existingResult = db.get('SELECT plot_point_id FROM book_plot_points WHERE author_id=? AND book_id=? AND hashed_title=?', [userId, bookId, hashedName]); + if (existingResult !== null) { + throw new Error(lang === 'fr' ? `Ce point de l'intrigue existe déjà.` : `This plot point already exists.`); + } + const insertResult: RunResult = db.run('INSERT INTO book_plot_points (plot_point_id,title,hashed_title,author_id,book_id,linked_incident_id,meta_plot) VALUES (?,?,?,?,?,?,?)', [plotPointId, encryptedName, hashedName, userId, bookId, incidentId, metaPlot]); + if (insertResult.changes > 0) { + return plotPointId; + } else { + throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du point d'intrigue.` : `Error adding plot point.`); + } + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + static deletePlotPoint(userId: string, plotNumId: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('DELETE FROM book_plot_points WHERE author_id=? AND plot_point_id=?', [userId, plotNumId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static insertNewIssue(issueId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, metaIssue: string, lang: 'fr' | 'en'): string { + try { + const db: Database = System.getDb(); + const result = db.get('SELECT issue_id FROM book_issues WHERE hashed_issue_name=? AND book_id=? AND author_id=?', [hashedName, bookId, userId]); + if (result !== null) { + throw new Error(lang === 'fr' ? `La problématique existe déjà.` : `This issue already exists.`); + } + const insertResult: RunResult = db.run('INSERT INTO book_issues (issue_id,author_id, book_id, name, hashed_issue_name, meta_issue) VALUES (?,?,?,?,?,?)', [issueId, userId, bookId, encryptedName, hashedName, metaIssue]); + if (insertResult.changes > 0) { + return issueId; + } else { + throw new Error(lang === 'fr' ? `Erreur pendant l'ajout de la problématique.` : `Error adding issue.`); + } + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static deleteIssue(userId: string, issueId: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('DELETE FROM book_issues WHERE author_id=? AND issue_id=?', [userId, issueId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static updateIncident(userId: string, bookId: string, incidentId: string, encryptedIncidentName: string, incidentHashedName: string, incidentSummary: string, meta: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('UPDATE book_incidents SET title=?, hashed_title=?, summary=?, meta_incident=? WHERE author_id=? AND book_id=? AND incident_id=?', [encryptedIncidentName, incidentHashedName, incidentSummary, meta, userId, bookId, incidentId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static updatePlotPoint(userId: string, bookId: string, plotPointId: string, encryptedPlotPointName: string, plotPointHashedName: string, plotPointSummary: string, meta: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('UPDATE book_plot_points SET title=?, hashed_title=?, summary=?, meta_plot=? WHERE author_id=? AND book_id=? AND plot_point_id=?', [encryptedPlotPointName, plotPointHashedName, plotPointSummary, meta, userId, bookId, plotPointId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + public static checkWorldExist(userId:string,bookId:string,worldName:string, lang: 'fr' | 'en'):boolean{ + try { + const db: Database = System.getDb(); + const result = db.get('SELECT world_id FROM book_world WHERE author_id=? AND book_id=? AND hashed_name=?', [userId,bookId,worldName]); + return result !== null; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static insertNewWorld(worldId: string, userId: string, bookId: string, encryptedName: string, hashedName: string, meta: string, lang: 'fr' | 'en'): string { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('INSERT INTO book_world (world_id,author_id, book_id, name, hashed_name, meta_world) VALUES (?,?,?,?,?,?)', [worldId, userId, bookId, encryptedName, hashedName, meta]); + if (result.changes > 0) { + return worldId; + } else { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du monde.` : `Error adding world.`); + } + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + public static fetchWorlds(userId: string, bookId: string, lang: 'fr' | 'en'):WorldQuery[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT world.world_id AS world_id, world.name AS world_name,world.history, world.politics, world.economy, world.religion, world.languages, world.meta_world, element.element_id AS element_id,element.name AS element_name, element.description AS element_description, element.element_type, element.meta_element 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=?', [userId, bookId]) as WorldQuery[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static updateWorld(userId: string, worldId: string, encryptName: string, hashedName: string, encryptHistory: string, encryptPolitics: string, encryptEconomy: string, encryptReligion: string, encryptLanguages: string, meta: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('UPDATE book_world SET name=?, hashed_name=?, history=?, politics=?, economy=?, religion=?, languages=?, meta_world=? WHERE author_id=? AND world_id=?', [encryptName, hashedName, encryptHistory, encryptPolitics, encryptEconomy, encryptReligion, encryptLanguages, meta, userId, worldId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static updateWorldElements(userId: string, elements: WorldElementValue[], lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + for (const element of elements) { + const result: RunResult = db.run('UPDATE book_world_elements SET name=?, description=?, element_type=?, meta_element=? WHERE user_id=? AND element_id=?', [element.name, element.description, element.type, element.meta, userId, element.id]); + if (result.changes <= 0) { + return false; + } + } + return true; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static checkElementExist(worldNumId: string, hashedName: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + const result = db.get('SELECT element_id FROM book_world_elements WHERE world_id=? AND original_name=?', [worldNumId, hashedName]); + return result !== null; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static insertNewElement(userId: string, elementId: string, elementType: number, worldId: string, encryptedName: string, hashedName: string, meta: string, lang: 'fr' | 'en'): string { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('INSERT INTO book_world_elements (element_id,world_id,user_id, name, original_name, element_type, meta_element) VALUES (?,?,?,?,?,?,?)', [elementId, worldId, userId, encryptedName, hashedName, elementType, meta]); + if (result.changes > 0) { + return elementId; + } else { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout de l'élément.` : `Error adding element.`); + } + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static deleteElement(userId: string, elementId: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('DELETE FROM book_world_elements WHERE user_id=? AND element_id=?', [userId, elementId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static deleteBook(userId: string, bookId: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('DELETE FROM erit_books WHERE author_id=? AND book_id=?', [userId,bookId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de supprimer le livre.` : `Unable to delete book.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertAIGuideLine(userId: string, bookId: string, narrativeType: number, dialogueType: number, encryptedPlotSummary: string, encryptedToneAtmosphere: string, verbTense: number, language: number, encryptedThemes: string, metaGuideLine: string, lang: 'fr' | 'en'): boolean { + try { + const db: Database = System.getDb(); + let result: RunResult = db.run('UPDATE book_ai_guide_line SET narrative_type=?, dialogue_type=?, global_resume=?, atmosphere=?, verbe_tense=?, langue=?, themes=?, meta=? WHERE user_id=? AND book_id=?', [narrativeType ? narrativeType : null, dialogueType ? dialogueType : null, encryptedPlotSummary, encryptedToneAtmosphere, verbTense ? verbTense : null, language ? language : null, encryptedThemes, metaGuideLine, userId, bookId]); + if (result.changes > 0) { + return true; + } else { + result = db.run('INSERT INTO book_ai_guide_line (user_id, book_id, global_resume, themes, verbe_tense, narrative_type, langue, dialogue_type, tone, atmosphere, current_resume, meta) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', [userId, bookId, encryptedPlotSummary, encryptedThemes, verbTense ? verbTense : null, narrativeType ? narrativeType : null, language ? language : null, dialogueType ? dialogueType : null, encryptedToneAtmosphere, encryptedToneAtmosphere, encryptedPlotSummary, metaGuideLine]); + return result.changes > 0; + } + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + static fetchGuideLineAI(userId: string, bookId: string, lang: 'fr' | 'en'): GuideLineAIQuery { + try { + const db: Database = System.getDb(); + const result = db.get('SELECT narrative_type, dialogue_type, global_resume, atmosphere, verbe_tense, langue, themes, current_resume, meta FROM book_ai_guide_line WHERE user_id=? AND book_id=?', [userId, bookId]) as GuideLineAIQuery | null; + if (!result) { + throw new Error(lang === 'fr' ? `Ligne directrice IA non trouvée.` : `AI guideline not found.`); + } + return result; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + static insertActSummary(actSummaryId: string, userId: string, bookId: string, actId: number, actSummary: string, meta: string, lang: 'fr' | 'en'): string { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('INSERT INTO book_act_summaries (act_sum_id, book_id, user_id, act_index, summary, meta_acts) VALUES (?,?,?,?,?,?)', [actSummaryId, bookId, userId, actId, actSummary, meta]); + if (result.changes > 0) { + return actSummaryId; + } else { + throw new Error(lang === 'fr' ? `Erreur lors de l'ajout du résumé de l'acte.` : `Error adding act summary.`); + } + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + static fetchCompleteBookChapters(id: string, lang: 'fr' | 'en'): ChapterBookResult[] { + try { + const db: Database = System.getDb(); + const result = db.all('SELECT title, chapter_order, meta_chapter, content.content, content.meta_chapter_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', [id]) as ChapterBookResult[]; + if (result.length === 0) { + throw new Error(lang === 'fr' ? `Aucun chapitre trouvé.` : `No chapters found.`); + } + return result; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les chapitres.` : `Unable to retrieve chapters.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } +} diff --git a/electron/database/repositories/chapter.repository.ts b/electron/database/repositories/chapter.repository.ts new file mode 100644 index 0000000..268078c --- /dev/null +++ b/electron/database/repositories/chapter.repository.ts @@ -0,0 +1,425 @@ +import {Database, RunResult, SQLiteValue} from 'node-sqlite3-wasm'; +import System from "../System"; + +export interface ChapterContentQueryResult extends Record{ + chapter_id: string; + version: number; + content: string; + words_count: number; + title: string; + content_meta: string; + meta_chapter: string; + chapter_order: number; +} + +export interface ContentQueryResult extends Record { + content: string, + meta_chapter_content: string, +} +export interface ChapterQueryResult extends Record{ + chapter_id: string; + title: string; + chapter_order:number; + meta_chapter: string; +} + +export interface ActChapterQuery extends Record{ + chapter_info_id: number; + chapter_id: string; + title: string; + chapter_order: number; + meta_chapter: string; + act_id: number; + incident_id: string | null; + plot_point_id: string | null; + summary: string; + goal: string; + meta_chapter_info: string; +} + +export interface CompanionContentQueryResult extends Record{ + version: number; + content: string; + words_count: number; + meta_chapter_content: string; +} + +export interface ChapterStoryQueryResult extends Record{ + chapter_info_id: number; + act_id: number; + summary:string; + meta_acts:string; + chapter_summary: string; + chapter_goal: string; + meta_chapter_info: string; + incident_id: number; + incident_title: string; + incident_summary: string; + meta_incident: string; + plot_point_id: number; + plot_title: string; + plot_summary: string; + meta_plot: string; +} + +export interface LastChapterResult extends Record{ + chapter_id: string; + version: number; +} + +export default class ChapterRepo{ + public static checkNameDuplication(userId:string,bookId:string,hashedTitle:string, lang: 'fr' | 'en' = 'fr'):boolean{ + try { + const db: Database = System.getDb(); + const result = db.get('SELECT chapter_id FROM book_chapters WHERE author_id=? AND book_id=? AND hashed_title=?', [userId,bookId,hashedTitle]); + return result !== null; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier la duplication du nom.` : `Unable to verify name duplication.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + public static insertChapter(chapterId: string, userId: string, bookId: string, title: string, hashedTitle: string, wordsCount: number, chapterOrder: number, meta: string, lang: 'fr' | 'en' = 'fr'): string { + let result: RunResult; + try { + const db: Database = System.getDb(); + result = db.run('INSERT INTO book_chapters (chapter_id,author_id, book_id, title, hashed_title, words_count, chapter_order, meta_chapter) VALUES (?,?,?,?,?,?,?,?)', [chapterId, userId, bookId, title, hashedTitle, wordsCount, chapterOrder, meta]); + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter le chapitre.` : `Unable to add chapter.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (result.changes > 0) { + return chapterId; + } else { + throw new Error(lang === 'fr' ? `Une erreur s'est passé lors de l'ajout du chapitre.` : `Error adding chapter.`); + } + } + + public static fetchWholeChapter(userId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): ChapterContentQueryResult { + let result: 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, chapter.meta_chapter, content.content AS content, content.version as version, content.meta_chapter_content as content_meta 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 = ?'; + result = db.get(query, [version, chapterId, userId]) as ChapterContentQueryResult | null; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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 (!result) { + throw new Error(lang === 'fr' ? `Aucun chapitre trouvé avec cet ID.` : `No chapter found with this ID.`); + } + return result; + } + + 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.meta_chapter, 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`; + return db.all(query, [userId, bookId]) as ChapterContentQueryResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + public static fetchAllChapterForActs(userId:string,bookId:string, lang: 'fr' | 'en' = 'fr'):ActChapterQuery[]{ + try { + const db: Database = System.getDb(); + return db.all('SELECT ci.chapter_info_id AS chapter_info_id, ci.chapter_id AS chapter_id, chapter.title, chapter.chapter_order, chapter.meta_chapter, ci.act_id, ci.incident_id AS incident_id, ci.plot_point_id AS plot_point_id, ci.summary, ci.goal, ci.meta_chapter_info 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 = ?', [bookId, userId]) as ActChapterQuery[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les chapitres pour les actes.` : `Unable to retrieve chapters for acts.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + public static fetchAllChapterFromABook(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): ChapterQueryResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT chapter_id as chapter_id, title, chapter_order, meta_chapter FROM book_chapters WHERE book_id=? AND author_id=? ORDER BY chapter_order', [bookId, userId]) as ChapterQueryResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer les chapitres.` : `Unable to retrieve chapters.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + public static deleteChapter(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('DELETE FROM book_chapters WHERE author_id=? AND chapter_id=?', [userId, chapterId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de supprimer le chapitre.` : `Unable to delete chapter.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static insertChapterInformation(chapterInfoId: string, userId: string, chapterId: string, actId: number, bookId: string, plotId: string | null, incidentId: string | null, lang: 'fr' | 'en' = 'fr'): string { + let existResult; + let result: RunResult; + try { + const db: Database = System.getDb(); + existResult = db.get('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=?', [chapterId, actId, bookId, plotId, incidentId, userId]); + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence de l'information du chapitre.` : `Unable to verify chapter information existence.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (existResult !== null) { + throw new Error(lang === 'fr' ? `Le chapitre est déjà lié.` : `Chapter is already linked.`); + } + try { + const db: Database = System.getDb(); + result = db.run('INSERT INTO book_chapter_infos (chapter_info_id,chapter_id, act_id, book_id, author_id, incident_id, plot_point_id, summary, goal, meta_chapter_info) VALUES (?,?,?,?,?,?,?,?,?,?)', [chapterInfoId, chapterId, actId, bookId, userId, incidentId, plotId, '', '', '']); + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'ajouter l'information du chapitre.` : `Unable to add chapter information.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + if (result.changes > 0) { + return chapterInfoId; + } else { + throw new Error(lang === 'fr' ? `Une erreur s'est produite pendant la liaison du chapitre.` : `Error linking chapter.`); + } + } + + public static updateChapter(userId: string, chapterId: string, encryptedTitle: string, hashTitle: string, chapterOrder: number, meta: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('UPDATE book_chapters SET title=?, hashed_title=?, chapter_order=?, meta_chapter=? WHERE author_id=? AND chapter_id=?', [encryptedTitle, hashTitle, chapterOrder, meta, userId, chapterId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour le chapitre.` : `Unable to update chapter.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + public static updateChapterInfos(userId: string, chapterId: string, actId: number, bookId: string, incidentId: string | null, plotId: string | null, summary: string, goal: string | null, meta: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + let sql: string = `UPDATE book_chapter_infos + SET summary=?, + goal=?, + meta_chapter_info=? + WHERE chapter_id = ? + AND act_id = ? + AND book_id = ?`; + const params: any[] = [summary, goal, meta, chapterId, actId, bookId]; + if (incidentId) { + sql += ` AND incident_id=?`; + params.push(incidentId); + } else { + sql += ` AND incident_id IS NULL`; + } + if (plotId) { + sql += ` AND plot_point_id=?`; + params.push(plotId); + } else { + sql += ` AND plot_point_id IS NULL`; + } + sql += ` AND author_id=?`; + params.push(userId); + const result: RunResult = db.run(sql, params); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de mettre à jour les informations du chapitre.` : `Unable to update chapter information.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + public static updateChapterContent(userId: string, chapterId: string, version: number, encryptContent: string, wordsCount: number, meta: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('UPDATE book_chapter_content SET content=?, meta_chapter_content=?, words_count=? WHERE chapter_id=? AND author_id=? AND version=?', [encryptContent, meta, wordsCount, chapterId, userId, version]); + if (result.changes > 0) { + return true; + } else { + const contentId:string = System.createUniqueId(); + const insertResult: RunResult = db.run('INSERT INTO book_chapter_content (content_id,chapter_id, author_id, version, content, words_count, meta_chapter_content) VALUES (?,?,?,?,?,?,?)', [contentId, chapterId, userId, version, encryptContent, wordsCount, meta]); + return insertResult.changes > 0; + } + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + static fetchCompanionContent(userId: string, chapterIdNum: string, versionNum: number, lang: 'fr' | 'en' = 'fr'): CompanionContentQueryResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT version, content, words_count, meta_chapter_content FROM book_chapter_content WHERE author_id=? AND chapter_id=? AND version=?', [userId, chapterIdNum, versionNum]) as CompanionContentQueryResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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."); + } + } + } + + public static fetchLastChapter(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): LastChapterResult | null { + try { + const db: Database = System.getDb(); + const result = db.get('SELECT chapter_id as chapter_id,version FROM user_last_chapter WHERE user_id=? AND book_id=?', [userId, bookId]) as LastChapterResult | null; + return result; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer le dernier chapitre ouvert.` : `Unable to retrieve last opened chapter.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + public static updateLastChapterRecord(userId: string, bookId: string, chapterId: string, version: number, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('UPDATE user_last_chapter SET chapter_id=?, version=? WHERE user_id=? AND book_id=?', [chapterId, version, userId, bookId]); + if (result.changes > 0) { + return true; + } else { + const insertResult: RunResult = db.run('INSERT INTO user_last_chapter (user_id, book_id, chapter_id, version) VALUES (?,?,?,?)', [userId, bookId, chapterId, version]); + return insertResult.changes > 0; + } + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible d'enregistrer le dernier chapitre.` : `Unable to save last chapter.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + public static fetchChapterStory(userId: string, chapterId: string, lang: 'fr' | 'en' = 'fr'):ChapterStoryQueryResult[] { + try { + const db: Database = System.getDb(); + return db.all('SELECT chapter_info_id,chapter.act_id,act_sum.summary,act_sum.meta_acts,chapter.summary AS chapter_summary,chapter.goal AS chapter_goal,meta_chapter_info,chapter.incident_id,incident.title AS incident_title, incident.summary AS incident_summary,incident.meta_incident,chapter.plot_point_id,plot.title AS plot_title,plot.summary AS plot_summary,plot.meta_plot 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=?', [chapterId, userId]) as ChapterStoryQueryResult[]; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de récupérer l'histoire du chapitre.` : `Unable to retrieve chapter story.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } + + static fetchChapterContentByChapterOrder(userId: string, chapterOrder: number, bookId: string, lang: 'fr' | 'en' = 'fr'): ContentQueryResult { + let result: ContentQueryResult | null; + try { + const db: Database = System.getDb(); + result = db.get('SELECT content.content,content.meta_chapter_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=?', [chapterOrder, bookId, userId]) as ContentQueryResult | null; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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 (!result) { + throw new Error(lang === 'fr' ? `Aucun chapitre trouvé avec cet ordre.` : `No chapter found with this order.`); + } + return result; + } + + static fetchChapterContentByVersion(userId: string, chapterid: string, version: number, lang: 'fr' | 'en' = 'fr'): ContentQueryResult { + let result: ContentQueryResult | null; + try { + const db: Database = System.getDb(); + result = db.get('SELECT content, meta_chapter_content FROM book_chapter_content WHERE author_id=? AND chapter_id=? AND version=?', [userId, chapterid, version]) as ContentQueryResult | null; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.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 (!result) { + throw new Error(lang === 'fr' ? `Aucun chapitre trouvé avec cette version.` : `No chapter found with this version.`); + } + return result; + } + + static deleteChapterInformation(userId: string, chapterInfoId: string, lang: 'fr' | 'en' = 'fr'): boolean { + try { + const db: Database = System.getDb(); + const result: RunResult = db.run('DELETE FROM book_chapter_infos WHERE chapter_info_id=? AND author_id=?', [chapterInfoId, userId]); + return result.changes > 0; + } catch (e: unknown) { + if (e instanceof Error) { + console.error(`DB Error: ${e.message}`); + throw new Error(lang === 'fr' ? `Impossible de supprimer les informations du chapitre.` : `Unable to delete chapter information.`); + } else { + console.error("An unknown error occurred."); + throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : "An unknown error occurred."); + } + } + } +}