Files
proxdeploy/src/betrieb/migrieren.ts
T
florian.wildeandClaude Opus 5.5 533a821087 feat(betrieb): Docker Compose und LXC-Installer
Ein Laufzeit-Paket (scripts/paket-bauen.sh) für beide Wege: Next standalone, worker als Bundle mit nur den nötigen Paketen, Migrationen ohne Prisma-CLI. Compose lokal komplett hochgefahren; der Installer auf pve-demo in einem echten Debian-13-LXC durchgelaufen.

Closes #019

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:35:41 +02:00

95 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createHash, randomUUID } from "node:crypto";
import { readdirSync, readFileSync } from "node:fs";
import { join } from "node:path";
import pg from "pg";
/**
* Spielt die Migrationen aus `prisma/migrations/` ein – im Betrieb statt `prisma migrate deploy`,
* damit weder Docker-Image noch LXC das Prisma-CLI samt Schema-Engine brauchen.
*
* Kompatibel zu Prisma: dieselbe Tabelle `_prisma_migrations`, Prüfsumme = SHA-256 der
* migration.sql. In der Entwicklung bleibt `prisma migrate dev` also benutzbar, und beide
* erkennen, was schon eingespielt ist.
*
* Jede Migration läuft in einer eigenen Transaktion; eine Sperre (`pg_advisory_lock`) verhindert,
* dass app und worker gleichzeitig migrieren. Eine eingespielte Migration, deren Datei sich
* geändert hat, ist ein Fehler – dann stimmt der Codestand nicht zur Datenbank.
*/
const SPERRE = 724_196_331; // beliebig, aber fest: „proxdeploy-migration“
export type MigrationsErgebnis = { eingespielt: string[]; vorhanden: number };
export async function migrieren(
datenbankUrl: string,
ordner: string,
log: (text: string) => void = () => {},
): Promise<MigrationsErgebnis> {
const migrationen = readdirSync(ordner, { withFileTypes: true })
.filter((e) => e.isDirectory() && /^\d{14}_/.test(e.name))
.map((e) => e.name)
.sort();
const client = new pg.Client({ connectionString: datenbankUrl });
await client.connect();
try {
await client.query("SELECT pg_advisory_lock($1)", [SPERRE]);
await client.query(`CREATE TABLE IF NOT EXISTS "_prisma_migrations" (
"id" VARCHAR(36) PRIMARY KEY NOT NULL,
"checksum" VARCHAR(64) NOT NULL,
"finished_at" TIMESTAMPTZ,
"migration_name" VARCHAR(255) NOT NULL,
"logs" TEXT,
"rolled_back_at" TIMESTAMPTZ,
"started_at" TIMESTAMPTZ NOT NULL DEFAULT now(),
"applied_steps_count" INTEGER NOT NULL DEFAULT 0
)`);
const { rows } = await client.query<{
migration_name: string;
checksum: string;
finished_at: Date | null;
}>(`SELECT migration_name, checksum, finished_at FROM "_prisma_migrations" WHERE rolled_back_at IS NULL`);
const bekannt = new Map(rows.map((r) => [r.migration_name, r]));
const unfertig = rows.filter((r) => !r.finished_at);
if (unfertig.length > 0) {
throw new Error(
`Migration ${unfertig.map((r) => r.migration_name).join(", ")} ist unvollständig eingespielt – bitte prüfen.`,
);
}
const eingespielt: string[] = [];
for (const name of migrationen) {
const sql = readFileSync(join(ordner, name, "migration.sql"));
const pruefsumme = createHash("sha256").update(sql).digest("hex");
const vorhanden = bekannt.get(name);
if (vorhanden) {
if (vorhanden.checksum !== pruefsumme) {
throw new Error(`Migration ${name} wurde nach dem Einspielen verändert (Prüfsumme weicht ab).`);
}
continue;
}
log(`Migration ${name} …`);
await client.query("BEGIN");
try {
const id = randomUUID();
await client.query(
`INSERT INTO "_prisma_migrations" (id, checksum, migration_name, started_at) VALUES ($1, $2, $3, now())`,
[id, pruefsumme, name],
);
await client.query(sql.toString("utf8"));
await client.query(
`UPDATE "_prisma_migrations" SET finished_at = now(), applied_steps_count = 1 WHERE id = $1`,
[id],
);
await client.query("COMMIT");
} catch (fehler) {
await client.query("ROLLBACK");
throw new Error(`Migration ${name} fehlgeschlagen: ${(fehler as Error).message}`);
}
eingespielt.push(name);
}
return { eingespielt, vorhanden: migrationen.length - eingespielt.length };
} finally {
await client.query("SELECT pg_advisory_unlock($1)", [SPERRE]).catch(() => {});
await client.end();
}
}