Add database schema, encryption utilities, and local database service
- Implement `schema.ts` for SQLite schema creation, indexing, and sync metadata initialization. - Develop `encryption.ts` with AES-256-GCM encryption utilities for securing database data. - Add `database.service.ts` to manage CRUD operations with encryption support, user-specific databases, and schema initialization. - Integrate book, chapter, and character operations with encrypted content handling and sync preparation.
This commit is contained in:
525
electron/database/schema.ts
Normal file
525
electron/database/schema.ts
Normal file
@@ -0,0 +1,525 @@
|
||||
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 = 1;
|
||||
|
||||
/**
|
||||
* 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,
|
||||
meta_message TEXT 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,
|
||||
meta_acts TEXT NOT NULL,
|
||||
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,
|
||||
meta 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 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,
|
||||
meta_chapter TEXT NOT NULL,
|
||||
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,
|
||||
meta_chapter_content TEXT 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,
|
||||
meta_chapter_info 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,
|
||||
char_meta TEXT NOT NULL,
|
||||
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,
|
||||
attr_meta 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,
|
||||
meta_guide_line 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,
|
||||
meta_incident TEXT NOT NULL,
|
||||
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,
|
||||
meta_issue 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,
|
||||
loc_meta 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,
|
||||
meta_plot 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,
|
||||
meta_world TEXT NOT NULL,
|
||||
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,
|
||||
meta_element TEXT NOT NULL,
|
||||
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,
|
||||
book_meta 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,
|
||||
user_meta TEXT NOT NULL,
|
||||
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,
|
||||
element_meta TEXT NOT NULL,
|
||||
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,
|
||||
sub_elem_meta TEXT NOT NULL,
|
||||
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');
|
||||
}
|
||||
Reference in New Issue
Block a user