v0.2.01: automatische migraties bij opstarten (transactie per bestand, geen dataverlies)
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 17s
All checks were successful
dev - build & deploy naar test / build-and-deploy (push) Successful in 17s
This commit is contained in:
parent
352e22a64b
commit
08fd9e11e3
4 changed files with 52 additions and 1 deletions
|
|
@ -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 package.json ./
|
||||||
COPY --chown=app:app src ./src
|
COPY --chown=app:app src ./src
|
||||||
COPY --chown=app:app public ./public
|
COPY --chown=app:app public ./public
|
||||||
|
COPY --chown=app:app db ./db
|
||||||
|
|
||||||
# Versie wordt tijdens de build meegegeven door CI
|
# Versie wordt tijdens de build meegegeven door CI
|
||||||
ARG APP_VERSION=dev
|
ARG APP_VERSION=dev
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/* teach - kern: versie, i18n, accounts, opslag, geluid */
|
/* teach - kern: versie, i18n, accounts, opslag, geluid */
|
||||||
"use strict";
|
"use strict";
|
||||||
/* version — bump on every change (form 0.0.00) */
|
/* version — bump on every change (form 0.0.00) */
|
||||||
const VERSION = "0.2.00";
|
const VERSION = "0.2.01";
|
||||||
(function(){
|
(function(){
|
||||||
const tag = document.getElementById("verTag");
|
const tag = document.getElementById("verTag");
|
||||||
tag.textContent = "v"+VERSION;
|
tag.textContent = "v"+VERSION;
|
||||||
|
|
|
||||||
48
src/migrate.js
Normal file
48
src/migrate.js
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import Fastify from 'fastify';
|
||||||
import fastifyStatic from '@fastify/static';
|
import fastifyStatic from '@fastify/static';
|
||||||
import pg from 'pg';
|
import pg from 'pg';
|
||||||
import api, { bootstrapSuper } from './api.js';
|
import api, { bootstrapSuper } from './api.js';
|
||||||
|
import { runMigrations } from './migrate.js';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
|
@ -53,6 +54,7 @@ app.register(fastifyStatic, {
|
||||||
// --- Start -------------------------------------------------------------------
|
// --- Start -------------------------------------------------------------------
|
||||||
const start = async () => {
|
const start = async () => {
|
||||||
try {
|
try {
|
||||||
|
await runMigrations(pool, app.log, join(__dirname, '..', 'db'));
|
||||||
await bootstrapSuper(pool, app.log);
|
await bootstrapSuper(pool, app.log);
|
||||||
await app.listen({ port: PORT, host: HOST });
|
await app.listen({ port: PORT, host: HOST });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue