import sqlite3 from 'node-sqlite3-wasm'; type Database = sqlite3.Database; /** * SQLite schema based on the MySQL erit_main_db schema * All tables use snake_case naming to match the server database * Data is encrypted before storage and decrypted on retrieval */ export const SCHEMA_VERSION = 2; /** * Initialize the local SQLite database with all required tables * @param db - SQLite database instance */ export function initializeSchema(db: Database): void { // Enable foreign keys db.exec('PRAGMA foreign_keys = ON'); // Create sync metadata table (tracks last sync times) db.exec(` CREATE TABLE IF NOT EXISTS _sync_metadata ( table_name TEXT PRIMARY KEY, last_sync_at INTEGER NOT NULL, last_push_at INTEGER, pending_changes INTEGER DEFAULT 0 ); `); // Create pending changes queue (for offline operations) db.exec(` CREATE TABLE IF NOT EXISTS _pending_changes ( id INTEGER PRIMARY KEY AUTOINCREMENT, table_name TEXT NOT NULL, operation TEXT NOT NULL, -- 'INSERT', 'UPDATE', 'DELETE' record_id TEXT NOT NULL, data TEXT, -- JSON data for INSERT/UPDATE created_at INTEGER NOT NULL, retry_count INTEGER DEFAULT 0 ); `); // AI Conversations db.exec(` CREATE TABLE IF NOT EXISTS ai_conversations ( conversation_id TEXT PRIMARY KEY, book_id TEXT NOT NULL, mode TEXT NOT NULL, title TEXT NOT NULL, start_date INTEGER NOT NULL, status INTEGER NOT NULL, user_id TEXT NOT NULL, summary TEXT, convo_meta TEXT NOT NULL, synced INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); // AI Messages History db.exec(` CREATE TABLE IF NOT EXISTS ai_messages_history ( message_id TEXT PRIMARY KEY, conversation_id TEXT NOT NULL, role TEXT NOT NULL, message TEXT NOT NULL, message_date INTEGER NOT NULL, synced INTEGER DEFAULT 0, FOREIGN KEY (conversation_id) REFERENCES ai_conversations(conversation_id) ON DELETE CASCADE ); `); // Book Acts db.exec(` CREATE TABLE IF NOT EXISTS book_acts ( act_id INTEGER PRIMARY KEY, title TEXT NOT NULL ); `); // Book Act Summaries db.exec(` CREATE TABLE IF NOT EXISTS book_act_summaries ( act_sum_id TEXT PRIMARY KEY, book_id TEXT NOT NULL, user_id TEXT NOT NULL, act_index INTEGER NOT NULL, summary TEXT, synced INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); // Book AI Guide Line db.exec(` CREATE TABLE IF NOT EXISTS book_ai_guide_line ( user_id TEXT NOT NULL, book_id TEXT NOT NULL, global_resume TEXT, themes TEXT, verbe_tense INTEGER, narrative_type INTEGER, langue INTEGER, dialogue_type INTEGER, tone TEXT, atmosphere TEXT, current_resume TEXT, synced INTEGER DEFAULT 0, PRIMARY KEY (user_id, book_id), FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); // Book Chapters db.exec(` CREATE TABLE IF NOT EXISTS book_chapters ( chapter_id TEXT PRIMARY KEY, book_id TEXT NOT NULL, author_id TEXT NOT NULL, title TEXT NOT NULL, hashed_title TEXT, words_count INTEGER, chapter_order INTEGER, synced INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); // Book Chapter Content db.exec(` CREATE TABLE IF NOT EXISTS book_chapter_content ( content_id TEXT PRIMARY KEY, chapter_id TEXT NOT NULL, author_id TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 2, content TEXT NOT NULL, words_count INTEGER NOT NULL, time_on_it INTEGER NOT NULL DEFAULT 0, synced INTEGER DEFAULT 0, FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE ); `); // Book Chapter Infos db.exec(` CREATE TABLE IF NOT EXISTS book_chapter_infos ( chapter_info_id TEXT PRIMARY KEY, chapter_id TEXT, act_id INTEGER, incident_id TEXT, plot_point_id TEXT, book_id TEXT, author_id TEXT, summary TEXT NOT NULL, goal TEXT NOT NULL, synced INTEGER DEFAULT 0, FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE, FOREIGN KEY (incident_id) REFERENCES book_incidents(incident_id) ON DELETE CASCADE, FOREIGN KEY (plot_point_id) REFERENCES book_plot_points(plot_point_id) ON DELETE CASCADE ); `); // Book Characters db.exec(` CREATE TABLE IF NOT EXISTS book_characters ( character_id TEXT PRIMARY KEY, book_id TEXT NOT NULL, user_id TEXT NOT NULL, first_name TEXT NOT NULL, last_name TEXT, category TEXT NOT NULL, title TEXT, image TEXT, role TEXT, biography TEXT, history TEXT, synced INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); // Book Character Attributes db.exec(` CREATE TABLE IF NOT EXISTS book_characters_attributes ( attr_id TEXT PRIMARY KEY, character_id TEXT NOT NULL, user_id TEXT NOT NULL, attribute_name TEXT NOT NULL, attribute_value TEXT NOT NULL, synced INTEGER DEFAULT 0, FOREIGN KEY (character_id) REFERENCES book_characters(character_id) ON DELETE CASCADE ); `); // Book Character Relations db.exec(` CREATE TABLE IF NOT EXISTS book_characters_relations ( rel_id INTEGER PRIMARY KEY, character_id INTEGER NOT NULL, char_name TEXT NOT NULL, type TEXT NOT NULL, description TEXT NOT NULL, history TEXT NOT NULL, synced INTEGER DEFAULT 0 ); `); // Book Guide Line db.exec(` CREATE TABLE IF NOT EXISTS book_guide_line ( user_id TEXT NOT NULL, book_id TEXT NOT NULL, tone TEXT NOT NULL, atmosphere TEXT NOT NULL, writing_style TEXT NOT NULL, themes TEXT NOT NULL, symbolism TEXT NOT NULL, motifs TEXT NOT NULL, narrative_voice TEXT NOT NULL, pacing TEXT NOT NULL, intended_audience TEXT NOT NULL, key_messages TEXT NOT NULL, synced INTEGER DEFAULT 0, PRIMARY KEY (user_id, book_id), FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); // Book Incidents db.exec(` CREATE TABLE IF NOT EXISTS book_incidents ( incident_id TEXT PRIMARY KEY, author_id TEXT NOT NULL, book_id TEXT NOT NULL, title TEXT NOT NULL, hashed_title TEXT NOT NULL, summary TEXT, synced INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); // Book Issues db.exec(` CREATE TABLE IF NOT EXISTS book_issues ( issue_id TEXT PRIMARY KEY, author_id TEXT NOT NULL, book_id TEXT NOT NULL, name TEXT NOT NULL, hashed_issue_name TEXT NOT NULL, synced INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); // Book Location db.exec(` CREATE TABLE IF NOT EXISTS book_location ( loc_id TEXT PRIMARY KEY, book_id TEXT NOT NULL, user_id TEXT NOT NULL, loc_name TEXT NOT NULL, loc_original_name TEXT NOT NULL, synced INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); // Book Plot Points db.exec(` CREATE TABLE IF NOT EXISTS book_plot_points ( plot_point_id TEXT PRIMARY KEY, title TEXT NOT NULL, hashed_title TEXT NOT NULL, summary TEXT, linked_incident_id TEXT, author_id TEXT NOT NULL, book_id TEXT NOT NULL, synced INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); // Book World db.exec(` CREATE TABLE IF NOT EXISTS book_world ( world_id TEXT PRIMARY KEY, name TEXT NOT NULL, hashed_name TEXT NOT NULL, author_id TEXT NOT NULL, book_id TEXT NOT NULL, history TEXT, politics TEXT, economy TEXT, religion TEXT, languages TEXT, synced INTEGER DEFAULT 0, FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE ); `); // Book World Elements db.exec(` CREATE TABLE IF NOT EXISTS book_world_elements ( element_id TEXT PRIMARY KEY, world_id TEXT NOT NULL, user_id TEXT NOT NULL, element_type INTEGER NOT NULL, name TEXT NOT NULL, original_name TEXT NOT NULL, description TEXT, synced INTEGER DEFAULT 0, FOREIGN KEY (world_id) REFERENCES book_world(world_id) ON DELETE CASCADE ); `); // Erit Books db.exec(` CREATE TABLE IF NOT EXISTS erit_books ( book_id TEXT PRIMARY KEY, type TEXT NOT NULL, author_id TEXT NOT NULL, title TEXT NOT NULL, hashed_title TEXT NOT NULL, sub_title TEXT, hashed_sub_title TEXT NOT NULL, summary TEXT NOT NULL, serie_id INTEGER, desired_release_date TEXT, desired_word_count INTEGER, words_count INTEGER, cover_image TEXT, synced INTEGER DEFAULT 0 ); `); // Erit Book Series db.exec(` CREATE TABLE IF NOT EXISTS erit_book_series ( serie_id INTEGER PRIMARY KEY, name TEXT NOT NULL, author_id INTEGER NOT NULL ); `); // Erit Editor Settings db.exec(` CREATE TABLE IF NOT EXISTS erit_editor ( user_id TEXT, type TEXT NOT NULL, text_size INTEGER NOT NULL, text_intent INTEGER NOT NULL, interline TEXT NOT NULL, paper_width INTEGER NOT NULL, theme TEXT NOT NULL, focus INTEGER NOT NULL, synced INTEGER DEFAULT 0 ); `); // Erit Users db.exec(` CREATE TABLE IF NOT EXISTS erit_users ( user_id TEXT PRIMARY KEY, first_name TEXT NOT NULL, last_name TEXT NOT NULL, username TEXT NOT NULL, email TEXT NOT NULL, origin_email TEXT NOT NULL, origin_username TEXT NOT NULL, author_name TEXT, origin_author_name TEXT, plateform TEXT NOT NULL, social_id TEXT, user_group INTEGER NOT NULL DEFAULT 4, password TEXT, term_accepted INTEGER NOT NULL DEFAULT 0, verify_code TEXT, reg_date INTEGER NOT NULL, account_verified INTEGER NOT NULL DEFAULT 0, erite_points INTEGER NOT NULL DEFAULT 100, stripe_customer_id TEXT, credits_balance REAL DEFAULT 0, synced INTEGER DEFAULT 0 ); `); // Location Element db.exec(` CREATE TABLE IF NOT EXISTS location_element ( element_id TEXT PRIMARY KEY, location TEXT NOT NULL, user_id TEXT NOT NULL, element_name TEXT NOT NULL, original_name TEXT NOT NULL, element_description TEXT, synced INTEGER DEFAULT 0, FOREIGN KEY (location) REFERENCES book_location(loc_id) ON DELETE CASCADE ); `); // Location Sub Element db.exec(` CREATE TABLE IF NOT EXISTS location_sub_element ( sub_element_id TEXT PRIMARY KEY, element_id TEXT NOT NULL, user_id TEXT NOT NULL, sub_elem_name TEXT NOT NULL, original_name TEXT NOT NULL, sub_elem_description TEXT, synced INTEGER DEFAULT 0, FOREIGN KEY (element_id) REFERENCES location_element(element_id) ON DELETE CASCADE ); `); // User Keys db.exec(` CREATE TABLE IF NOT EXISTS user_keys ( user_id TEXT NOT NULL, brand TEXT NOT NULL, key TEXT NOT NULL, actif INTEGER NOT NULL DEFAULT 1, synced INTEGER DEFAULT 0, FOREIGN KEY (user_id) REFERENCES erit_users(user_id) ON DELETE CASCADE ); `); // User Last Chapter db.exec(` CREATE TABLE IF NOT EXISTS user_last_chapter ( user_id TEXT NOT NULL, book_id TEXT NOT NULL, chapter_id TEXT NOT NULL, version INTEGER NOT NULL, synced INTEGER DEFAULT 0, PRIMARY KEY (user_id, book_id), FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE, FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE ); `); // Create indexes for better performance createIndexes(db); // Initialize sync metadata for all tables initializeSyncMetadata(db); } /** * Create indexes for frequently queried columns */ function createIndexes(db: Database): void { db.exec(` CREATE INDEX IF NOT EXISTS idx_ai_conversations_book ON ai_conversations(book_id); CREATE INDEX IF NOT EXISTS idx_ai_conversations_user ON ai_conversations(user_id); CREATE INDEX IF NOT EXISTS idx_ai_messages_conversation ON ai_messages_history(conversation_id); CREATE INDEX IF NOT EXISTS idx_chapters_book ON book_chapters(book_id); CREATE INDEX IF NOT EXISTS idx_chapter_content_chapter ON book_chapter_content(chapter_id); CREATE INDEX IF NOT EXISTS idx_characters_book ON book_characters(book_id); CREATE INDEX IF NOT EXISTS idx_character_attrs_character ON book_characters_attributes(character_id); CREATE INDEX IF NOT EXISTS idx_world_book ON book_world(book_id); CREATE INDEX IF NOT EXISTS idx_world_elements_world ON book_world_elements(world_id); CREATE INDEX IF NOT EXISTS idx_pending_changes_table ON _pending_changes(table_name); CREATE INDEX IF NOT EXISTS idx_pending_changes_created ON _pending_changes(created_at); `); } /** * Initialize sync metadata for all tables */ function initializeSyncMetadata(db: Database): void { const tables = [ 'ai_conversations', 'ai_messages_history', 'book_acts', 'book_act_summaries', 'book_ai_guide_line', 'book_chapters', 'book_chapter_content', 'book_chapter_infos', 'book_characters', 'book_characters_attributes', 'book_guide_line', 'book_incidents', 'book_issues', 'book_location', 'book_plot_points', 'book_world', 'book_world_elements', 'erit_books', 'erit_editor', 'erit_users', 'location_element', 'location_sub_element', 'user_keys', 'user_last_chapter' ]; for (const table of tables) { db.run(` INSERT OR IGNORE INTO _sync_metadata (table_name, last_sync_at, pending_changes) VALUES (?, 0, 0) `, [table]); } } /** * Drop all tables (for testing/reset) */ export function dropAllTables(db: Database): void { const tables = db.all(` SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' `, []) as unknown as { name: string }[]; db.exec('PRAGMA foreign_keys = OFF'); for (const { name } of tables) { db.exec(`DROP TABLE IF EXISTS ${name}`); } db.exec('PRAGMA foreign_keys = ON'); } /** * Get current schema version from database */ function getDbSchemaVersion(db: Database): number { try { const result = db.get('SELECT version FROM _schema_version LIMIT 1') as { version: number } | undefined; return result?.version ?? 0; } catch { return 0; } } /** * Set schema version in database */ function setDbSchemaVersion(db: Database, version: number): void { db.exec('CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER PRIMARY KEY)'); db.run('DELETE FROM _schema_version'); db.run('INSERT INTO _schema_version (version) VALUES (?)', [version]); } /** * Check if a column exists in a table */ function columnExists(db: Database, tableName: string, columnName: string): boolean { const columns = db.all(`PRAGMA table_info(${tableName})`) as { name: string }[]; return columns.some(col => col.name === columnName); } /** * Safely drop a column if it exists */ function dropColumnIfExists(db: Database, tableName: string, columnName: string): void { if (columnExists(db, tableName, columnName)) { try { db.exec(`ALTER TABLE ${tableName} DROP COLUMN ${columnName}`); console.log(`[Migration] Dropped column ${columnName} from ${tableName}`); } catch (e) { console.error(`[Migration] Failed to drop column ${columnName} from ${tableName}:`, e); } } } /** * Run migrations to update schema from one version to another */ export function runMigrations(db: Database): void { const currentVersion = getDbSchemaVersion(db); if (currentVersion >= SCHEMA_VERSION) { return; } console.log(`[Migration] Upgrading schema from version ${currentVersion} to ${SCHEMA_VERSION}`); // Migration 1 -> 2: Remove all meta_* columns if (currentVersion < 2) { console.log('[Migration] Running migration v2: Removing meta columns...'); dropColumnIfExists(db, 'ai_messages_history', 'meta_message'); dropColumnIfExists(db, 'book_act_summaries', 'meta_acts'); dropColumnIfExists(db, 'book_ai_guide_line', 'meta'); dropColumnIfExists(db, 'book_chapters', 'meta_chapter'); dropColumnIfExists(db, 'book_chapter_content', 'meta_chapter_content'); dropColumnIfExists(db, 'book_chapter_infos', 'meta_chapter_info'); dropColumnIfExists(db, 'book_characters', 'char_meta'); dropColumnIfExists(db, 'book_characters_attributes', 'attr_meta'); dropColumnIfExists(db, 'book_guide_line', 'meta_guide_line'); dropColumnIfExists(db, 'book_incidents', 'meta_incident'); dropColumnIfExists(db, 'book_issues', 'meta_issue'); dropColumnIfExists(db, 'book_location', 'loc_meta'); dropColumnIfExists(db, 'book_plot_points', 'meta_plot'); dropColumnIfExists(db, 'book_world', 'meta_world'); dropColumnIfExists(db, 'book_world_elements', 'meta_element'); dropColumnIfExists(db, 'erit_books', 'book_meta'); dropColumnIfExists(db, 'erit_users', 'user_meta'); dropColumnIfExists(db, 'location_element', 'element_meta'); dropColumnIfExists(db, 'location_sub_element', 'sub_elem_meta'); console.log('[Migration] Migration v2 completed'); } // Update schema version setDbSchemaVersion(db, SCHEMA_VERSION); console.log(`[Migration] Schema updated to version ${SCHEMA_VERSION}`); }