fix(api): migrations idempotentes pour bases existantes (metadata/automation)

Les colonnes metadata_source_config et automation_settings sont
désormais ajoutées par ALTER TABLE conditionnels sur les bases déjà
initialisées, l'index books_isbn13_idx est créé via le chemin de
migration, et un test couvre la base de données existante.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Git Agent
2026-08-23 13:11:06 +02:00
parent 62abf890e0
commit 0fa2f99289
2 changed files with 168 additions and 1 deletions

View File

@ -122,7 +122,6 @@ export class DatabaseService implements OnModuleDestroy {
CREATE INDEX IF NOT EXISTS books_library_idx ON books(library_id);
CREATE INDEX IF NOT EXISTS books_title_idx ON books(title);
CREATE INDEX IF NOT EXISTS books_isbn13_idx ON books(isbn13);
CREATE INDEX IF NOT EXISTS jobs_status_idx ON jobs(status);
CREATE TRIGGER IF NOT EXISTS books_ai AFTER INSERT ON books BEGIN
@ -144,6 +143,8 @@ export class DatabaseService implements OnModuleDestroy {
`);
this.ensureBooksSupportsComicArchives();
this.ensureBooksMetadataColumns();
this.ensureMetadataSourceConfigColumns();
this.ensureAutomationSettingsColumns();
this.ensureMetadataDefaults();
this.sqlite.exec("INSERT INTO book_fts(book_fts) VALUES('rebuild')");
}
@ -234,6 +235,59 @@ export class DatabaseService implements OnModuleDestroy {
this.sqlite.exec("CREATE INDEX IF NOT EXISTS books_isbn13_idx ON books(isbn13)");
}
private ensureMetadataSourceConfigColumns(): void {
const names = this.columnNames("metadata_source_config");
const now = sqlString(this.now());
if (!names.has("enabled")) {
this.sqlite.exec("ALTER TABLE metadata_source_config ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1");
}
if (!names.has("priority")) {
this.sqlite.exec("ALTER TABLE metadata_source_config ADD COLUMN priority INTEGER NOT NULL DEFAULT 0");
}
if (!names.has("api_key")) {
this.sqlite.exec("ALTER TABLE metadata_source_config ADD COLUMN api_key TEXT");
}
if (!names.has("created_at")) {
this.sqlite.exec(`ALTER TABLE metadata_source_config ADD COLUMN created_at TEXT NOT NULL DEFAULT ${now}`);
}
if (!names.has("updated_at")) {
this.sqlite.exec(`ALTER TABLE metadata_source_config ADD COLUMN updated_at TEXT NOT NULL DEFAULT ${now}`);
}
}
private ensureAutomationSettingsColumns(): void {
const names = this.columnNames("automation_settings");
const now = sqlString(this.now());
const disabledScan = sqlString(JSON.stringify({ frequency: "disabled", time: "03:00", dayOfWeek: 1 }));
const disabledEnrich = sqlString(JSON.stringify({ frequency: "disabled", time: "04:00", dayOfWeek: 1 }));
if (!names.has("watch_libraries")) {
this.sqlite.exec("ALTER TABLE automation_settings ADD COLUMN watch_libraries INTEGER NOT NULL DEFAULT 0");
}
if (!names.has("auto_enrich_new_books")) {
this.sqlite.exec("ALTER TABLE automation_settings ADD COLUMN auto_enrich_new_books INTEGER NOT NULL DEFAULT 1");
}
if (!names.has("isbn_priority_enabled")) {
this.sqlite.exec("ALTER TABLE automation_settings ADD COLUMN isbn_priority_enabled INTEGER NOT NULL DEFAULT 1");
}
if (!names.has("scan_schedule_json")) {
this.sqlite.exec(`ALTER TABLE automation_settings ADD COLUMN scan_schedule_json TEXT NOT NULL DEFAULT ${disabledScan}`);
}
if (!names.has("enrich_schedule_json")) {
this.sqlite.exec(`ALTER TABLE automation_settings ADD COLUMN enrich_schedule_json TEXT NOT NULL DEFAULT ${disabledEnrich}`);
}
if (!names.has("created_at")) {
this.sqlite.exec(`ALTER TABLE automation_settings ADD COLUMN created_at TEXT NOT NULL DEFAULT ${now}`);
}
if (!names.has("updated_at")) {
this.sqlite.exec(`ALTER TABLE automation_settings ADD COLUMN updated_at TEXT NOT NULL DEFAULT ${now}`);
}
}
private columnNames(table: string): Set<string> {
const columns = this.sqlite.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
return new Set(columns.map((column) => column.name));
}
private ensureMetadataDefaults(): void {
const now = this.now();
const insertSource = this.sqlite.prepare(`
@ -265,3 +319,7 @@ export class DatabaseService implements OnModuleDestroy {
);
}
}
function sqlString(value: string): string {
return `'${value.replace(/'/g, "''")}'`;
}