cards/apps/api/scripts/bootstrap-drizzle-tracking.ts
Till JS 4bb1390180
Some checks are pending
CI / validate (push) Waiting to run
db(cards): baseline migration + drizzle-tracking bootstrap script
Schließt die Ops-Lücke „kein versioniertes Schema-Tracking" aus
FEATURE_IDEAS.md.

* apps/api/src/db/migrations/0000_baseline.sql — Drizzle-generierte
  Baseline-Migration, 355 Zeilen, 25 Tabellen + 5 Enums (cards- und
  marketplace-Schema). Eingefrostet auf den Live-Stand 2026-05-12.
* apps/api/scripts/bootstrap-drizzle-tracking.ts — neues Script,
  markiert die Baseline in einer bestehenden DB als „bereits
  angewandt", ohne SQL erneut auszuführen. Verwendet sha256 wie
  drizzle-orm/migrator (Hash 312d67ba1aeb…), idempotent.
* package.json: drizzle:migrate + drizzle:bootstrap-tracking
  npm-scripts.
* docs/playbooks/DRIZZLE_MIGRATIONS_BOOTSTRAP.md — Hand-Over für
  Prod (Bootstrap einmalig, dann normaler Workflow:
  schema → generate → commit → migrate, kein push --force mehr).

Lokal verifiziert: 17/104 Tests grün, bootstrap idempotent,
drizzle-kit migrate erkennt die Baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:53:52 +02:00

90 lines
3 KiB
TypeScript

/**
* Bootstrap-Script für drizzle.__drizzle_migrations.
*
* Eine Cards-DB, die bisher über `drizzle-kit push` (Schema-Sync ohne
* Migrations-Tracking) gepflegt wurde, hat das Drizzle-Tracking-Schema
* nicht. Dieses Script holt das nach:
*
* 1. Erstellt `drizzle.__drizzle_migrations` (idempotent).
* 2. Liest `src/db/migrations/meta/_journal.json`.
* 3. Markiert jede dort gelistete Migration als „bereits angewandt",
* mit dem gleichen Hash, den `drizzle-orm/migrator` selbst
* berechnen würde — sha256(file_content) als hex.
*
* Nutzung:
* DATABASE_URL=postgresql://… pnpm bootstrap:drizzle
*
* Idempotent: ein zweiter Run macht nichts neu, sondern überspringt
* Migrations, deren Hash schon eingetragen ist.
*
* Nach dem Bootstrap wird `drizzle-kit migrate` jede Migration aus
* dem Journal als bekannt erkennen und keine SQL erneut ausführen.
* Künftige Schema-Änderungen ⇒ `drizzle-kit generate` ⇒ commit ⇒
* `drizzle-kit migrate` führt nur die neuen Migrations aus.
*/
import { createHash } from 'node:crypto';
import { readFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import postgres from 'postgres';
const url = process.env.DATABASE_URL;
if (!url) {
console.error('DATABASE_URL not set');
process.exit(1);
}
const here = dirname(fileURLToPath(import.meta.url));
const migrationsDir = resolve(here, '..', 'src', 'db', 'migrations');
const journalPath = resolve(migrationsDir, 'meta', '_journal.json');
interface JournalEntry {
idx: number;
version: string;
when: number;
tag: string;
breakpoints: boolean;
}
interface Journal {
version: string;
dialect: string;
entries: JournalEntry[];
}
const journal: Journal = JSON.parse(readFileSync(journalPath, 'utf-8'));
const sql = postgres(url, { max: 1 });
try {
await sql`CREATE SCHEMA IF NOT EXISTS drizzle`;
await sql`CREATE TABLE IF NOT EXISTS drizzle.__drizzle_migrations (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at bigint
)`;
const existing = await sql<{ hash: string }[]>`SELECT hash FROM drizzle.__drizzle_migrations`;
const existingHashes = new Set(existing.map((r) => r.hash));
for (const entry of journal.entries) {
const migrationPath = resolve(migrationsDir, `${entry.tag}.sql`);
const fileContent = readFileSync(migrationPath, 'utf-8');
const hash = createHash('sha256').update(fileContent).digest('hex');
if (existingHashes.has(hash)) {
console.log(`SKIP ${entry.tag} (hash ${hash.slice(0, 12)}… already tracked)`);
continue;
}
await sql`INSERT INTO drizzle.__drizzle_migrations ("hash", "created_at")
VALUES (${hash}, ${entry.when})`;
console.log(`MARKED ${entry.tag} (hash ${hash.slice(0, 12)}…)`);
}
const final = await sql<{ count: number }[]>`SELECT COUNT(*)::int AS count
FROM drizzle.__drizzle_migrations`;
console.log(`\nTracking-Tabelle hat jetzt ${final[0]!.count} Eintrag/Einträge.`);
} finally {
await sql.end({ timeout: 5 });
}