/** * Migrationstest. * * Legt eine Wegwerf-Datenbank an, spielt alle Migrationen der Reihe nach ein * und prüft anschließend, ob das Ergebnis genau dem Schema entspricht. Damit * fällt auf, wenn jemand das Schema ändert, ohne eine Migration zu erzeugen — * beim Aufspielen im Betrieb wäre das sonst erst spät zu merken. * * Aufruf: npm run test:migration */ import 'dotenv/config' import { spawnSync } from 'node:child_process' import { join } from 'node:path' import { Client } from 'pg' const WURZEL = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1') const verbindung = process.env.DATABASE_URL if (!verbindung) { console.error('DATABASE_URL ist nicht gesetzt.') process.exit(1) } const testDatenbank = `planer_migrationstest_${Date.now()}` /** Dieselbe Verbindung, aber auf eine andere Datenbank gerichtet. */ function mitDatenbank(url: string, name: string): string { const zerlegt = new URL(url) zerlegt.pathname = `/${name}` return zerlegt.toString() } function prisma(argumente: string[], zusatz: Record): number { const ergebnis = spawnSync( process.execPath, [join(WURZEL, 'node_modules', 'prisma', 'build', 'index.js'), ...argumente], { cwd: WURZEL, stdio: 'inherit', env: { ...process.env, ...zusatz }, }, ) return ergebnis.status ?? 1 } async function aufVerwaltungsdatenbank( tun: (klient: Client) => Promise, ): Promise { // "postgres" ist auf jedem Server vorhanden und wird hier nur benutzt, um // die Wegwerf-Datenbank anzulegen und wieder zu entfernen. const klient = new Client({ connectionString: mitDatenbank(verbindung!, 'postgres') }) await klient.connect() try { return await tun(klient) } finally { await klient.end() } } async function main(): Promise { console.log(`▸ Wegwerf-Datenbank ${testDatenbank} anlegen`) await aufVerwaltungsdatenbank((k) => k.query(`CREATE DATABASE "${testDatenbank}"`)) const testUrl = mitDatenbank(verbindung!, testDatenbank) console.log('▸ Alle Migrationen einspielen') if (prisma(['migrate', 'deploy'], { DATABASE_URL: testUrl }) !== 0) { console.error('✗ Die Migrationen ließen sich nicht einspielen.') return 1 } console.log('▸ Ergebnis mit dem Schema vergleichen') // --exit-code liefert 2, wenn ein Unterschied bleibt. const abweichung = prisma( [ 'migrate', 'diff', // Die Quelle ist die eben eingespielte Wegwerf-Datenbank, das Ziel das // Schema. So braucht der Vergleich keine Schattendatenbank. '--from-config-datasource', '--to-schema', 'prisma/schema.prisma', '--exit-code', ], { DATABASE_URL: testUrl }, ) if (abweichung === 2) { console.error( '\n✗ Nach dem Einspielen weicht die Datenbank vom Schema ab.\n' + ' Vermutlich fehlt eine Migration: "npx prisma migrate dev --name ".', ) return 1 } if (abweichung !== 0) { console.error('✗ Der Vergleich ließ sich nicht durchführen.') return 1 } console.log('\n✓ Die Migrationen ergeben genau das Schema.') return 0 } let code = 1 try { code = await main() } catch (fehler) { console.error('Der Migrationstest ist abgebrochen:', fehler) } finally { await aufVerwaltungsdatenbank(async (k) => { // Offene Verbindungen trennen, sonst lässt sich die Datenbank nicht löschen. await k.query( 'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1', [testDatenbank], ) await k.query(`DROP DATABASE IF EXISTS "${testDatenbank}"`) }) console.log(`▸ Wegwerf-Datenbank ${testDatenbank} entfernt`) } process.exit(code)