Files
ERitors-Scribe-Desktop/electron/database/schema.ts
natreex 9648d9e9be Upgrade database schema to version 2 and remove unused meta_* columns
- Increment `SCHEMA_VERSION` to 2 in `schema.ts`.
- Remove all `meta_*` columns from database tables.
- Add migration logic to handle schema upgrades and clean up unused columns.
- Modify database models and repository methods to exclude `meta_*` fields for stricter typings and improved structure.
- Refactor and optimize query statements across repositories to align with new schema changes.
2025-11-26 19:17:40 -05:00

594 lines
20 KiB
TypeScript

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