Files
Florian-netz/src/db/seed/index.ts
Matthias Hochmeister 034fdb175f Workstream 9: Seed-Daten aus NÖ-Katalog (Phase 6)
Idempotente Katalog-Seeds, die docs/reference/fahrzeug-katalog-noelfv.md
als Code abbilden. Importiert ausschließlich das bestehende Drizzle-Schema
(WS2), definiert keine Tabellen neu.

- 34 Merkmale (+ Enum-Optionen: feuerloeschpumpe_typ=8, anzahl_achsen=3,
  stromerzeuger_bauart=3); Funkrufname ist Spalte, kein Merkmal.
- 11 Fahrzeug-Vorlagen mit Pflichtmerkmalen, typisierten Vorgabewerten
  (vorgabewert_num/_text/_bool) und Aliassen mit `bestaetigt`.
  RLF/RLFA 2000 + 2000-4000 = true; kein HLFA-Alias (Laufzeitregel ist
  kanonisch); HLF 4-U als Alias auf HLF 4 mit Pulver-Pflichtmerkmalen.
- 11 Geräte-Kategorien (Natural Key name).
- upsert.ts: ausschließlich onConflictDoUpdate auf Natural Keys
  (slug/code/name + Verknüpfungs-PKs); index.ts seedet in EINER Transaktion
  (Merkmale -> Optionen -> Vorlagen -> Vorlagen-Merkmale -> Aliasse ->
  Kategorien) via Slug->ID-Map, sequenzielle Awaits.
- Reiner Offline-Unit-Test (seed.test.ts) prüft alle fachlichen Invarianten
  ohne DB; package.json-Script db:seed ergänzt.

Verifikation offline: tsc --noEmit (0), drizzle-kit check (0),
next build (0), vitest run (191 passed, 7 DB-Tests skipped).
Seed-Ausführung selbst deferred (kein Postgres im Sandbox).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:07:39 +02:00

118 lines
3.8 KiB
TypeScript

import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "@/db/schema";
import type { Tx } from "@/lib/audit";
import { MERKMALE } from "./data/merkmale";
import { VEHICLE_TEMPLATES } from "./data/vehicle-templates";
import { EQUIPMENT_CATEGORIES } from "./data/equipment-categories";
import {
upsertMerkmal,
upsertVehicleTemplate,
upsertTemplateMerkmal,
upsertTemplateAlias,
upsertEquipmentCategory,
upsertCategoryMerkmal,
pruneTemplateAliasse,
} from "./upsert";
/**
* Katalog-Seed (Workstream 9): füllt Merkmale, Enum-Optionen, Fahrzeug-Vorlagen,
* deren Pflichtmerkmale + Aliasse sowie Geräte-Kategorien aus dem NÖ-Katalog.
*
* Idempotent (Querschnittsstandard 7): ausschließlich Upserts auf Natural Keys;
* mehrfaches Ausführen ändert keine Counts. Läuft in EINER Transaktion in der
* Reihenfolge Merkmale → Optionen → Vorlagen → Vorlagen-Merkmale → Aliasse →
* Kategorien (sequenzielle Awaits, Slug→ID-Map).
*
* Liest `DATABASE_URL` direkt aus der Umgebung (keine Next.js-Env-Validierung,
* wie scripts/migrate.ts und scripts/seed-auth.ts).
*/
/** Reine Seed-Logik gegen eine bestehende Transaktion (für Tests injizierbar). */
export async function seedCatalog(tx: Tx): Promise<void> {
// 1. Merkmale (+ Optionen) → Slug→ID-Map.
const merkmalIdBySlug = new Map<string, string>();
for (const m of MERKMALE) {
const id = await upsertMerkmal(tx, m);
merkmalIdBySlug.set(m.slug, id);
}
// 2. Vorlagen → Pflichtmerkmale → Aliasse.
for (let i = 0; i < VEHICLE_TEMPLATES.length; i++) {
const t = VEHICLE_TEMPLATES[i]!;
const templateId = await upsertVehicleTemplate(tx, t, i);
for (let j = 0; j < t.merkmale.length; j++) {
const tm = t.merkmale[j]!;
const merkmalId = merkmalIdBySlug.get(tm.slug);
if (!merkmalId) {
throw new Error(
`Vorlage ${t.code} referenziert unbekanntes Merkmal '${tm.slug}'`,
);
}
await upsertTemplateMerkmal(tx, templateId, merkmalId, tm, j);
}
for (const a of t.aliasse) {
await upsertTemplateAlias(tx, templateId, a.alias, a.bestaetigt);
}
await pruneTemplateAliasse(
tx,
templateId,
t.aliasse.map((a) => a.alias),
);
}
// 3. Geräte-Kategorien (+ optionale Merkmal-Verknüpfungen).
for (const c of EQUIPMENT_CATEGORIES) {
const categoryId = await upsertEquipmentCategory(tx, c);
const slugs = c.merkmalSlugs ?? [];
for (let k = 0; k < slugs.length; k++) {
const merkmalId = merkmalIdBySlug.get(slugs[k]!);
if (!merkmalId) {
throw new Error(
`Kategorie ${c.name} referenziert unbekanntes Merkmal '${slugs[k]}'`,
);
}
await upsertCategoryMerkmal(tx, categoryId, merkmalId, k);
}
}
}
/** Standalone-Runner (npm run db:seed). */
export async function main(): Promise<void> {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL ist nicht gesetzt.");
}
const pool = new Pool({ connectionString, max: 1 });
const db = drizzle(pool, { schema });
try {
await db.transaction(async (tx) => {
await seedCatalog(tx as Tx);
});
console.log("Katalog-Seed erfolgreich (idempotent).");
console.log(` Merkmale: ${MERKMALE.length}`);
console.log(` Fahrzeug-Vorlagen: ${VEHICLE_TEMPLATES.length}`);
console.log(` Geräte-Kategorien: ${EQUIPMENT_CATEGORIES.length}`);
} finally {
await pool.end();
}
}
// Nur ausführen, wenn direkt gestartet (nicht beim Import in Tests).
const isMain =
typeof process !== "undefined" &&
process.argv[1] !== undefined &&
import.meta.url === `file://${process.argv[1]}`;
if (isMain) {
main().catch((err: unknown) => {
console.error("Katalog-Seed fehlgeschlagen:", err);
process.exit(1);
});
}