41d4465905
Adds backend/src/db/migrations-runner.js: - Tracks applied migrations in _migrations table - Applies .sql files from src/db/migrations/ in alphabetical order - Each file runs in a transaction — fail-fast, no partial state - `migrate:bootstrap` marks 000_baseline.sql as applied on existing DBs 000_baseline.sql — full schema snapshot from prod DB (168 objects, 2026-05-06). Removed stale PostgreSQL migration files (001_init.sql, 002_constraints.sql) that used SERIAL/EXTENSION syntax incompatible with SQLite. npm scripts: migrate → migrations-runner.js (versioned) migrate:bootstrap → mark baseline applied (run once per env) migrate:legacy → legacy migrate.js (kept for reference) On prod DB after `migrate:bootstrap`: "Nothing to apply — schema is up to date". Legacy migrate.js still in place; tests still use it via setup.js (unchanged). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
84 lines
2.4 KiB
JavaScript
84 lines
2.4 KiB
JavaScript
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const db = require('./db');
|
|
|
|
const MIGRATIONS_DIR = path.join(__dirname, 'migrations');
|
|
|
|
function init() {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS _migrations (
|
|
filename TEXT PRIMARY KEY,
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
`);
|
|
}
|
|
|
|
function listFiles() {
|
|
if (!fs.existsSync(MIGRATIONS_DIR)) return [];
|
|
return fs.readdirSync(MIGRATIONS_DIR)
|
|
.filter(f => f.endsWith('.sql'))
|
|
.sort();
|
|
}
|
|
|
|
function applied() {
|
|
return new Set(db.prepare('SELECT filename FROM _migrations').all().map(r => r.filename));
|
|
}
|
|
|
|
function run() {
|
|
init();
|
|
const done = applied();
|
|
const files = listFiles();
|
|
const pending = files.filter(f => !done.has(f));
|
|
|
|
if (pending.length === 0) {
|
|
console.log('[migrate] Nothing to apply — schema is up to date');
|
|
return;
|
|
}
|
|
|
|
for (const file of pending) {
|
|
const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, file), 'utf8');
|
|
console.log(`[migrate] Applying ${file}...`);
|
|
try {
|
|
db.exec('BEGIN');
|
|
db.exec(sql);
|
|
db.prepare('INSERT INTO _migrations (filename) VALUES (?)').run(file);
|
|
db.exec('COMMIT');
|
|
console.log(`[migrate] OK: ${file}`);
|
|
} catch (e) {
|
|
db.exec('ROLLBACK');
|
|
console.error(`[migrate] FAILED at ${file}: ${e.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
console.log(`[migrate] Applied ${pending.length} migration(s)`);
|
|
}
|
|
|
|
/* Mark 000_baseline.sql as applied on existing DBs without running it.
|
|
Run once per environment after first deploy of this system. */
|
|
function markBaseline() {
|
|
init();
|
|
const hasUsers = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='users'").get();
|
|
if (hasUsers) {
|
|
const done = applied();
|
|
if (!done.has('000_baseline.sql')) {
|
|
db.prepare('INSERT INTO _migrations (filename) VALUES (?)').run('000_baseline.sql');
|
|
console.log('[migrate] Marked 000_baseline.sql as applied (existing DB — not re-run)');
|
|
} else {
|
|
console.log('[migrate] 000_baseline.sql already marked as applied');
|
|
}
|
|
} else {
|
|
console.log('[migrate] No users table found — run `npm run migrate` first to initialize schema');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const cmd = process.argv[2];
|
|
if (cmd === 'bootstrap') markBaseline();
|
|
else run();
|
|
}
|
|
|
|
module.exports = { run, markBaseline };
|