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>
This commit is contained in:
117
src/db/seed/index.ts
Normal file
117
src/db/seed/index.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user