diff --git a/Dockerfile b/Dockerfile index 196fea0..311376e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,6 +19,7 @@ COPY --chown=app:app --from=deps /app/node_modules ./node_modules COPY --chown=app:app package.json ./ COPY --chown=app:app src ./src COPY --chown=app:app public ./public +COPY --chown=app:app db ./db # Versie wordt tijdens de build meegegeven door CI ARG APP_VERSION=dev diff --git a/public/js/core.js b/public/js/core.js index 4f6d6da..2f3ecfd 100644 --- a/public/js/core.js +++ b/public/js/core.js @@ -1,7 +1,7 @@ /* teach - kern: versie, i18n, accounts, opslag, geluid */ "use strict"; /* version — bump on every change (form 0.0.00) */ -const VERSION = "0.2.00"; +const VERSION = "0.2.01"; (function(){ const tag = document.getElementById("verTag"); tag.textContent = "v"+VERSION; diff --git a/src/migrate.js b/src/migrate.js new file mode 100644 index 0000000..a9b3a65 --- /dev/null +++ b/src/migrate.js @@ -0,0 +1,48 @@ +// 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(); + } +} diff --git a/src/server.js b/src/server.js index 233f13d..e2314ef 100644 --- a/src/server.js +++ b/src/server.js @@ -4,6 +4,7 @@ import Fastify from 'fastify'; import fastifyStatic from '@fastify/static'; import pg from 'pg'; import api, { bootstrapSuper } from './api.js'; +import { runMigrations } from './migrate.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -53,6 +54,7 @@ app.register(fastifyStatic, { // --- Start ------------------------------------------------------------------- const start = async () => { try { + await runMigrations(pool, app.log, join(__dirname, '..', 'db')); await bootstrapSuper(pool, app.log); await app.listen({ port: PORT, host: HOST }); } catch (err) {