// Automatische, veilige migraties bij het opstarten. // // Veiligheid: // - alleen vooruit: elk .sql-bestand in db/ wordt precies één keer uitgevoerd // en daarna geregistreerd in schema_migrations; // - elk bestand draait in zijn eigen transactie: mislukt er iets, dan wordt // alles van dat bestand teruggedraaid en start de app NIET (bestaande data // blijft onaangeroerd); // - een advisory lock voorkomt dat twee instanties tegelijk migreren; // - de migratiebestanden zelf zijn idempotent (IF NOT EXISTS), dus ook een // database waarop ze al handmatig zijn uitgevoerd blijft gewoon werken. import { readdir, readFile } from 'node:fs/promises'; import { join } from 'node:path'; const LOCK_KEY = 727271; export async function runMigrations(pool, log, dir) { const client = await pool.connect(); try { await client.query('SELECT pg_advisory_lock($1)', [LOCK_KEY]); await client.query(`CREATE TABLE IF NOT EXISTS schema_migrations ( filename TEXT PRIMARY KEY, applied_at TIMESTAMPTZ NOT NULL DEFAULT now() )`); const done = new Set( (await client.query('SELECT filename FROM schema_migrations')).rows.map((r) => r.filename), ); const files = (await readdir(dir)).filter((f) => f.endsWith('.sql')).sort(); for (const f of files) { if (done.has(f)) continue; const sql = await readFile(join(dir, f), 'utf8'); log.info(`migratie uitvoeren: ${f}`); try { await client.query('BEGIN'); await client.query(sql); await client.query('INSERT INTO schema_migrations (filename) VALUES ($1)', [f]); await client.query('COMMIT'); log.info(`migratie klaar: ${f}`); } catch (e) { await client.query('ROLLBACK'); throw new Error(`migratie ${f} mislukt (teruggedraaid, data ongewijzigd): ${e.message}`); } } } finally { await client.query('SELECT pg_advisory_unlock($1)', [LOCK_KEY]).catch(() => {}); client.release(); } }