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:
218
src/db/seed/upsert.ts
Normal file
218
src/db/seed/upsert.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import type { Tx } from "@/lib/audit";
|
||||
import * as schema from "@/db/schema";
|
||||
import type { MerkmalSeed } from "./data/merkmale";
|
||||
import type { VehicleTemplateSeed } from "./data/vehicle-templates";
|
||||
import type { EquipmentCategorySeed } from "./data/equipment-categories";
|
||||
|
||||
/**
|
||||
* Idempotente Upserts für Workstream-9-Seeds (Querschnittsstandard 7:
|
||||
* ausschließlich `onConflictDoUpdate` auf Natural Keys, mehrfaches Ausführen
|
||||
* ändert keine Counts).
|
||||
*
|
||||
* Natural Keys:
|
||||
* - merkmale.slug
|
||||
* - merkmal_optionen(merkmalId, wert)
|
||||
* - vehicle_templates.code
|
||||
* - vehicle_template_merkmale(templateId, merkmalId) [PK]
|
||||
* - vehicle_template_aliasse(templateId, alias)
|
||||
* - equipment_categories.name
|
||||
* - equipment_category_merkmale(categoryId, merkmalId) [PK]
|
||||
*
|
||||
* Alle Funktionen nehmen die laufende Transaktion `tx`, damit der gesamte Seed
|
||||
* atomar bleibt.
|
||||
*/
|
||||
|
||||
/** Upsert eines Merkmals (+ Enum-Optionen). Liefert die Merkmal-UUID. */
|
||||
export async function upsertMerkmal(tx: Tx, m: MerkmalSeed): Promise<string> {
|
||||
const [row] = await tx
|
||||
.insert(schema.merkmale)
|
||||
.values({
|
||||
slug: m.slug,
|
||||
name: m.name,
|
||||
typ: m.typ,
|
||||
einheit: m.einheit ?? null,
|
||||
geltungsbereich: m.geltungsbereich,
|
||||
// Katalog-Merkmale gelten unmittelbar als aktiv (kein Vorschlag).
|
||||
status: "active",
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: schema.merkmale.slug,
|
||||
set: {
|
||||
name: m.name,
|
||||
typ: m.typ,
|
||||
einheit: m.einheit ?? null,
|
||||
geltungsbereich: m.geltungsbereich,
|
||||
status: "active",
|
||||
},
|
||||
})
|
||||
.returning({ id: schema.merkmale.id });
|
||||
|
||||
if (!row) throw new Error(`Merkmal-Upsert ohne Rückgabe: ${m.slug}`);
|
||||
|
||||
for (const opt of m.optionen ?? []) {
|
||||
await tx
|
||||
.insert(schema.merkmalOptionen)
|
||||
.values({
|
||||
merkmalId: row.id,
|
||||
wert: opt.wert,
|
||||
label: opt.label,
|
||||
reihenfolge: opt.reihenfolge,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [schema.merkmalOptionen.merkmalId, schema.merkmalOptionen.wert],
|
||||
set: { label: opt.label, reihenfolge: opt.reihenfolge },
|
||||
});
|
||||
}
|
||||
|
||||
return row.id;
|
||||
}
|
||||
|
||||
/** Upsert einer Vorlage (ohne Merkmale/Aliasse). Liefert die Template-UUID. */
|
||||
export async function upsertVehicleTemplate(
|
||||
tx: Tx,
|
||||
t: VehicleTemplateSeed,
|
||||
reihenfolge: number,
|
||||
): Promise<string> {
|
||||
const [row] = await tx
|
||||
.insert(schema.vehicleTemplates)
|
||||
.values({
|
||||
code: t.code,
|
||||
name: t.name,
|
||||
beschreibung: t.beschreibung ?? null,
|
||||
reihenfolge,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: schema.vehicleTemplates.code,
|
||||
set: { name: t.name, beschreibung: t.beschreibung ?? null, reihenfolge },
|
||||
})
|
||||
.returning({ id: schema.vehicleTemplates.id });
|
||||
|
||||
if (!row) throw new Error(`Vorlagen-Upsert ohne Rückgabe: ${t.code}`);
|
||||
return row.id;
|
||||
}
|
||||
|
||||
/** Upsert eines Vorlagen-Pflichtmerkmals (PK templateId+merkmalId). */
|
||||
export async function upsertTemplateMerkmal(
|
||||
tx: Tx,
|
||||
templateId: string,
|
||||
merkmalId: string,
|
||||
m: VehicleTemplateSeed["merkmale"][number],
|
||||
reihenfolge: number,
|
||||
): Promise<void> {
|
||||
await tx
|
||||
.insert(schema.vehicleTemplateMerkmale)
|
||||
.values({
|
||||
templateId,
|
||||
merkmalId,
|
||||
vorgabewertNum: m.vorgabewertNum ?? null,
|
||||
vorgabewertText: m.vorgabewertText ?? null,
|
||||
vorgabewertBool: m.vorgabewertBool ?? null,
|
||||
pflicht: m.pflicht ?? false,
|
||||
reihenfolge,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
schema.vehicleTemplateMerkmale.templateId,
|
||||
schema.vehicleTemplateMerkmale.merkmalId,
|
||||
],
|
||||
set: {
|
||||
vorgabewertNum: m.vorgabewertNum ?? null,
|
||||
vorgabewertText: m.vorgabewertText ?? null,
|
||||
vorgabewertBool: m.vorgabewertBool ?? null,
|
||||
pflicht: m.pflicht ?? false,
|
||||
reihenfolge,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Upsert eines Alias (Natural Key templateId+alias). */
|
||||
export async function upsertTemplateAlias(
|
||||
tx: Tx,
|
||||
templateId: string,
|
||||
alias: string,
|
||||
bestaetigt: boolean,
|
||||
): Promise<void> {
|
||||
await tx
|
||||
.insert(schema.vehicleTemplateAliasse)
|
||||
.values({ templateId, alias, bestaetigt })
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
schema.vehicleTemplateAliasse.templateId,
|
||||
schema.vehicleTemplateAliasse.alias,
|
||||
],
|
||||
set: { bestaetigt },
|
||||
});
|
||||
}
|
||||
|
||||
/** Upsert einer Geräte-Kategorie (Natural Key name). Liefert die UUID. */
|
||||
export async function upsertEquipmentCategory(
|
||||
tx: Tx,
|
||||
c: EquipmentCategorySeed,
|
||||
): Promise<string> {
|
||||
const [row] = await tx
|
||||
.insert(schema.equipmentCategories)
|
||||
.values({ name: c.name, reihenfolge: c.reihenfolge })
|
||||
.onConflictDoUpdate({
|
||||
target: schema.equipmentCategories.name,
|
||||
set: { reihenfolge: c.reihenfolge },
|
||||
})
|
||||
.returning({ id: schema.equipmentCategories.id });
|
||||
|
||||
if (!row) throw new Error(`Kategorie-Upsert ohne Rückgabe: ${c.name}`);
|
||||
return row.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert der Kategorie-Merkmal-Verknüpfung (PK categoryId+merkmalId).
|
||||
* Idempotent über `onConflictDoNothing` (keine zusätzlichen Felder zu
|
||||
* aktualisieren außer reihenfolge).
|
||||
*/
|
||||
export async function upsertCategoryMerkmal(
|
||||
tx: Tx,
|
||||
categoryId: string,
|
||||
merkmalId: string,
|
||||
reihenfolge: number,
|
||||
): Promise<void> {
|
||||
await tx
|
||||
.insert(schema.equipmentCategoryMerkmale)
|
||||
.values({ categoryId, merkmalId, reihenfolge })
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
schema.equipmentCategoryMerkmale.categoryId,
|
||||
schema.equipmentCategoryMerkmale.merkmalId,
|
||||
],
|
||||
set: { reihenfolge },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt Aliasse zu einer Vorlage, die NICHT mehr im Seed stehen
|
||||
* (z. B. nachdem ein Alias aus dem Katalog gestrichen wurde). Hält den
|
||||
* Aliasse-Bestand exakt deckungsgleich mit dem Seed und damit idempotent,
|
||||
* ohne verwaiste Aliasse zu hinterlassen.
|
||||
*/
|
||||
export async function pruneTemplateAliasse(
|
||||
tx: Tx,
|
||||
templateId: string,
|
||||
keepAliasse: readonly string[],
|
||||
): Promise<void> {
|
||||
const existing = await tx
|
||||
.select({ alias: schema.vehicleTemplateAliasse.alias })
|
||||
.from(schema.vehicleTemplateAliasse)
|
||||
.where(eq(schema.vehicleTemplateAliasse.templateId, templateId));
|
||||
|
||||
const keep = new Set(keepAliasse);
|
||||
for (const e of existing) {
|
||||
if (!keep.has(e.alias)) {
|
||||
await tx
|
||||
.delete(schema.vehicleTemplateAliasse)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.vehicleTemplateAliasse.templateId, templateId),
|
||||
eq(schema.vehicleTemplateAliasse.alias, e.alias),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user