← back to Nationalrealestate
db/migrate.ts
61 lines
/**
* Runs every .sql file in db/migrations/ in lexical order, idempotent.
* Records applied filenames in schema_migrations.
* Constraint: migration files must NOT contain their own BEGIN/COMMIT —
* the runner wraps each file in a transaction.
*/
import 'dotenv/config';
import { readFileSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { pool } from './pool.ts';
const __dirname = dirname(fileURLToPath(import.meta.url));
const migrationsDir = join(__dirname, 'migrations');
async function ensureLedger() {
await pool.query(`
CREATE TABLE IF NOT EXISTS schema_migrations (
filename TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
`);
}
async function applied(): Promise<Set<string>> {
const r = await pool.query<{ filename: string }>(`SELECT filename FROM schema_migrations`);
return new Set(r.rows.map((x) => x.filename));
}
async function main() {
await ensureLedger();
const seen = await applied();
const files = readdirSync(migrationsDir).filter((f) => f.endsWith('.sql')).sort();
for (const f of files) {
if (seen.has(f)) {
console.log(`[skip] ${f}`);
continue;
}
const sql = readFileSync(join(migrationsDir, f), 'utf8');
console.log(`[apply] ${f}`);
try {
await pool.query('BEGIN');
await pool.query(sql);
await pool.query(`INSERT INTO schema_migrations (filename) VALUES ($1) ON CONFLICT DO NOTHING`, [f]);
await pool.query('COMMIT');
console.log(`[done] ${f}`);
} catch (e: any) {
try { await pool.query('ROLLBACK'); } catch {}
console.error(`[fail] ${f}: ${e.message}`);
process.exit(1);
}
}
await pool.end();
console.log('migrations complete');
}
main().catch((e) => {
console.error(e);
process.exit(1);
});