80eca1bc7c
git-subtree-dir: cms/core git-subtree-split: 2f52ec7536ee87b4f097d169e1cb0f6a85aebe84
76 lines
3.1 KiB
JavaScript
76 lines
3.1 KiB
JavaScript
// Migration runner — applies plugin-declared migrations once each, tracked in
|
|
// public.schema_migrations. The plugin manager only *surfaces* migrations()
|
|
// (file URLs); this is what actually runs them, at boot.
|
|
//
|
|
// Design for a lean / DB-optional core:
|
|
// * No declared migrations (e.g. kgva: no plugins) → returns immediately, never
|
|
// opens a DB connection and never imports `pg`.
|
|
// * `exec` is injectable — an async (text, params?) => rows function — so the
|
|
// logic unit-tests without a real database. Omit it and the runner connects
|
|
// via `pg` to `databaseUrl` (lazy import; clear error if `pg` is missing).
|
|
// * Each migration runs once (id = "<plugin>/<file>") inside its own
|
|
// transaction; the file SQL is itself idempotent (see 001_dialog.sql), so a
|
|
// pre-existing DB (openbureau) is baselined cleanly on first run.
|
|
import { readFile } from 'node:fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
export async function runMigrations(manager, { databaseUrl, exec, log = console } = {}) {
|
|
const migs = manager.migrations();
|
|
if (!migs.length) return { applied: [], skipped: [], pending: false };
|
|
|
|
let close = async () => {};
|
|
if (!exec) {
|
|
if (!databaseUrl) {
|
|
log.warn?.(`migrate: ${migs.length} Migration(en) deklariert, aber DATABASE_URL fehlt — übersprungen.`);
|
|
return { applied: [], skipped: migs.map((m) => `${m.plugin}/${m.file}`), pending: true };
|
|
}
|
|
({ exec, close } = await pgExecutor(databaseUrl));
|
|
}
|
|
|
|
try {
|
|
await exec(`CREATE TABLE IF NOT EXISTS public.schema_migrations (
|
|
id text PRIMARY KEY,
|
|
plugin text NOT NULL,
|
|
applied_at timestamptz NOT NULL DEFAULT now()
|
|
)`);
|
|
|
|
const applied = [], skipped = [];
|
|
for (const m of migs) {
|
|
const id = `${m.plugin}/${m.file}`;
|
|
const done = await exec('SELECT 1 FROM public.schema_migrations WHERE id = $1', [id]);
|
|
if (done?.length) { skipped.push(id); continue; }
|
|
try {
|
|
const sql = await readFile(fileURLToPath(m.url), 'utf8');
|
|
await exec('BEGIN');
|
|
try {
|
|
await exec(sql);
|
|
await exec('INSERT INTO public.schema_migrations (id, plugin) VALUES ($1, $2)', [id, m.plugin]);
|
|
await exec('COMMIT');
|
|
} catch (e) {
|
|
await exec('ROLLBACK');
|
|
throw e;
|
|
}
|
|
} catch (e) {
|
|
throw new Error(`Migration ${id} fehlgeschlagen: ${e.message}`);
|
|
}
|
|
applied.push(id);
|
|
log.log?.(`migrate: angewandt ${id}`);
|
|
}
|
|
return { applied, skipped, pending: false };
|
|
} finally {
|
|
await close();
|
|
}
|
|
}
|
|
|
|
// Default executor: one pg client over databaseUrl. `pg` is imported lazily so a
|
|
// DB-less instance never pulls it in at runtime.
|
|
async function pgExecutor(databaseUrl) {
|
|
let pg;
|
|
try { pg = (await import('pg')).default; }
|
|
catch { throw new Error("migrate: Paket 'pg' fehlt (npm i pg) — für Plugin-Migrationen nötig."); }
|
|
const client = new pg.Client({ connectionString: databaseUrl });
|
|
await client.connect();
|
|
const exec = async (text, params) => (await client.query(text, params)).rows;
|
|
return { exec, close: () => client.end() };
|
|
}
|