Add support for syncing tool settings with lastUpdate and improve consistency
- Introduced `lastUpdate` field in `book_tools` for tracking changes. - Refactored tool enablement logic in `CharacterComponent`, `WorldSetting`, and `LocationComponent` for consistency. - Updated database schema and migration scripts for `book_tools` table. - Enhanced synchronization workflows to support new `lastUpdate` logic. - Adjusted related models, repositories, and IPC handlers for streamlined management. - Improved type safety and robustness in tool-related methods with additional checks.
This commit is contained in:
@@ -8,7 +8,128 @@ type Database = sqlite3.Database;
|
||||
* Data is encrypted before storage and decrypted on retrieval
|
||||
*/
|
||||
|
||||
export const SCHEMA_VERSION = 3;
|
||||
// =============================================================================
|
||||
// MIGRATIONS
|
||||
// =============================================================================
|
||||
|
||||
const schemaVersion = 1;
|
||||
|
||||
/**
|
||||
* DEV ONLY - S'exécute à chaque refresh, pas besoin de version
|
||||
* Mets ta query, test, efface après
|
||||
*/
|
||||
const devQueries: string[] = [
|
||||
// "ALTER TABLE book_tools ADD COLUMN last_update INTEGER DEFAULT 0",
|
||||
];
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
function columnExists(db: Database, table: string, column: string): boolean {
|
||||
const result = db.all(`PRAGMA table_info(${table})`) as { name: string }[];
|
||||
return result?.some((row) => row.name === column) ?? false;
|
||||
}
|
||||
|
||||
function addColumn(db: Database, table: string, column: string, type: string): void {
|
||||
if (!columnExists(db, table, column)) {
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getDbVersion(db: Database): number {
|
||||
const result = db.get('PRAGMA user_version') as { user_version: number } | undefined;
|
||||
return result?.user_version ?? 0;
|
||||
}
|
||||
|
||||
function setDbVersion(db: Database, version: number): void {
|
||||
db.exec(`PRAGMA user_version = ${version}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if old _schema_version table exists
|
||||
*/
|
||||
function hasOldSchemaTable(db: Database): boolean {
|
||||
const result = db.get(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='_schema_version'
|
||||
`) as { name: string } | undefined;
|
||||
return !!result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get version from old _schema_version table
|
||||
*/
|
||||
function getOldSchemaVersion(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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate from old _schema_version table to PRAGMA user_version
|
||||
* Old system: v3 = all migrations done (book_tools created, NOT NULL fixes applied)
|
||||
* New system: v1 = equivalent starting point
|
||||
*/
|
||||
function migrateFromOldSystem(db: Database): void {
|
||||
const oldVersion = getOldSchemaVersion(db);
|
||||
|
||||
// Old v3 means all previous migrations were done
|
||||
// Map to new system: old v3 = new v1
|
||||
if (oldVersion >= 3) {
|
||||
setDbVersion(db, schemaVersion);
|
||||
}
|
||||
|
||||
// Add last_update column to book_tools if missing (was added after v3)
|
||||
addColumn(db, 'book_tools', 'last_update', 'INTEGER DEFAULT 0');
|
||||
|
||||
// Drop old schema version table
|
||||
db.exec('DROP TABLE IF EXISTS _schema_version');
|
||||
}
|
||||
|
||||
export function runMigrations(db: Database): void {
|
||||
// DEV: run test queries (skip errors silently)
|
||||
if (isDev && devQueries.length > 0) {
|
||||
for (const query of devQueries) {
|
||||
try {
|
||||
db.exec(query);
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
// PROD only: versioned migrations
|
||||
if (isDev) return;
|
||||
|
||||
// Migrate from old _schema_version system to PRAGMA user_version
|
||||
if (hasOldSchemaTable(db)) {
|
||||
migrateFromOldSystem(db);
|
||||
return; // Migration done, no need to continue
|
||||
}
|
||||
|
||||
const currentVersion = getDbVersion(db);
|
||||
if (currentVersion >= schemaVersion) return;
|
||||
|
||||
// v1 - book_tools table with last_update (for fresh DBs or DBs without old system)
|
||||
if (currentVersion < 1) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS book_tools (
|
||||
book_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
characters_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
worlds_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
locations_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
last_update INTEGER DEFAULT 0,
|
||||
UNIQUE (book_id, user_id),
|
||||
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_book ON book_tools(book_id)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_user ON book_tools(user_id)`);
|
||||
}
|
||||
|
||||
setDbVersion(db, schemaVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the local SQLite database with all required tables
|
||||
@@ -420,187 +541,47 @@ export function initializeSchema(db: Database): void {
|
||||
characters_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
worlds_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
locations_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (book_id, user_id),
|
||||
last_update INTEGER DEFAULT 0,
|
||||
UNIQUE (book_id, user_id),
|
||||
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// Create indexes for better performance
|
||||
createIndexes(db);
|
||||
|
||||
// Set schema version for new databases (prevents unnecessary migrations)
|
||||
initializeSchemaVersion(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);
|
||||
`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_conversations_book ON ai_conversations(book_id)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_conversations_user ON ai_conversations(user_id)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_ai_messages_conversation ON ai_messages_history(conversation_id)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_chapters_book ON book_chapters(book_id)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_chapter_content_chapter ON book_chapter_content(chapter_id)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_characters_book ON book_characters(book_id)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_character_attrs_character ON book_characters_attributes(character_id)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_world_book ON book_world(book_id)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_world_elements_world ON book_world_elements(world_id)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_book ON book_tools(book_id)`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_book_tools_user ON book_tools(user_id)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current schema version from database
|
||||
* Drop all tables (for testing/reset)
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
export function dropAllTables(db: Database): void {
|
||||
const tables = db.all(`
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name NOT LIKE 'sqlite_%'
|
||||
`) as { name: string }[];
|
||||
|
||||
/**
|
||||
* 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]);
|
||||
}
|
||||
db.exec('PRAGMA foreign_keys = OFF');
|
||||
|
||||
/**
|
||||
* Initialize schema version for new databases
|
||||
* Only sets version if table doesn't exist yet (new DB)
|
||||
*/
|
||||
function initializeSchemaVersion(db: Database): void {
|
||||
const currentVersion = getDbSchemaVersion(db);
|
||||
if (currentVersion === 0) {
|
||||
setDbSchemaVersion(db, SCHEMA_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}`);
|
||||
} catch (e) {
|
||||
console.error(`[Migration] Failed to drop column ${columnName} from ${tableName}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreate a table with a new schema while preserving data
|
||||
*/
|
||||
function recreateTable(db: Database, tableName: string, newSchema: string, columnsToKeep: string): void {
|
||||
try {
|
||||
db.exec('PRAGMA foreign_keys = OFF');
|
||||
db.exec(`CREATE TABLE ${tableName}_backup AS SELECT ${columnsToKeep} FROM ${tableName}`);
|
||||
db.exec(`DROP TABLE ${tableName}`);
|
||||
db.exec(newSchema);
|
||||
db.exec(`INSERT INTO ${tableName} (${columnsToKeep}) SELECT ${columnsToKeep} FROM ${tableName}_backup`);
|
||||
db.exec(`DROP TABLE ${tableName}_backup`);
|
||||
db.exec('PRAGMA foreign_keys = ON');
|
||||
} catch (e) {
|
||||
console.error(`[Migration] Failed to recreate table ${tableName}:`, e);
|
||||
db.exec('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
for (const row of tables) {
|
||||
db.exec(`DROP TABLE IF EXISTS ${row.name}`);
|
||||
}
|
||||
|
||||
// Migration v2: Remove NOT NULL constraints to allow null values from server sync
|
||||
if (currentVersion < 2) {
|
||||
// Recreate erit_books with nullable hashed_sub_title and summary
|
||||
recreateTable(db, 'erit_books', `
|
||||
CREATE TABLE 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,
|
||||
summary TEXT,
|
||||
serie_id INTEGER,
|
||||
desired_release_date TEXT,
|
||||
desired_word_count INTEGER,
|
||||
words_count INTEGER,
|
||||
cover_image TEXT,
|
||||
last_update INTEGER DEFAULT 0
|
||||
)
|
||||
`, 'book_id, type, author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count, words_count, cover_image, last_update');
|
||||
|
||||
// Recreate book_chapter_content with nullable content
|
||||
recreateTable(db, 'book_chapter_content', `
|
||||
CREATE TABLE 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,
|
||||
words_count INTEGER NOT NULL,
|
||||
time_on_it INTEGER NOT NULL DEFAULT 0,
|
||||
last_update INTEGER DEFAULT 0,
|
||||
FOREIGN KEY (chapter_id) REFERENCES book_chapters(chapter_id) ON DELETE CASCADE
|
||||
)
|
||||
`, 'content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update');
|
||||
|
||||
// Recreate book_chapter_infos with nullable summary and goal
|
||||
recreateTable(db, 'book_chapter_infos', `
|
||||
CREATE TABLE 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,
|
||||
goal TEXT,
|
||||
last_update 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
|
||||
)
|
||||
`, 'chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update');
|
||||
}
|
||||
|
||||
if (currentVersion < 3) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS book_tools (
|
||||
book_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
characters_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
worlds_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
locations_enabled INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (book_id, user_id),
|
||||
FOREIGN KEY (book_id) REFERENCES erit_books(book_id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
// Update schema version
|
||||
setDbSchemaVersion(db, SCHEMA_VERSION);
|
||||
db.exec('PRAGMA foreign_keys = ON');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user