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:
@@ -14,6 +14,7 @@
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "tsx scripts/migrate.ts",
|
||||
"db:seed-auth": "tsx scripts/seed-auth.ts",
|
||||
"db:seed": "tsx src/db/seed/index.ts",
|
||||
"test:e2e:gating": "playwright test tests/e2e/auth-gating.spec.ts",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
|
||||
34
src/db/seed/data/equipment-categories.ts
Normal file
34
src/db/seed/data/equipment-categories.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Geräte-Kategorien als Seed-Daten (Workstream 9).
|
||||
*
|
||||
* Abgeleitet aus den Beladungs-Highlights in
|
||||
* docs/reference/fahrzeug-katalog-noelfv.md, Abschnitt 3.
|
||||
* Natural Key ist `name` (UNIQUE in `equipment_categories`).
|
||||
*
|
||||
* `merkmalSlugs` verweist optional auf Merkmale aus dem Katalog mit
|
||||
* Geltungsbereich `equipment`/`both` (z. B. hydraulischer Rettungssatz).
|
||||
*/
|
||||
|
||||
export interface EquipmentCategorySeed {
|
||||
name: string;
|
||||
reihenfolge: number;
|
||||
merkmalSlugs?: string[];
|
||||
}
|
||||
|
||||
export const EQUIPMENT_CATEGORIES: EquipmentCategorySeed[] = [
|
||||
{ name: "Löschgeräte", reihenfolge: 0 },
|
||||
{ name: "Schläuche & Armaturen", reihenfolge: 1 },
|
||||
{ name: "Atemschutz", reihenfolge: 2 },
|
||||
{
|
||||
name: "Technische Rettung",
|
||||
reihenfolge: 3,
|
||||
merkmalSlugs: ["hydraulischer_rettungssatz"],
|
||||
},
|
||||
{ name: "Beleuchtung & Stromerzeugung", reihenfolge: 4 },
|
||||
{ name: "Zug- & Anschlagmittel", reihenfolge: 5 },
|
||||
{ name: "Schadstoff & Gefahrgut", reihenfolge: 6 },
|
||||
{ name: "Atemluftversorgung", reihenfolge: 7 },
|
||||
{ name: "Logistik & Ladungssicherung", reihenfolge: 8 },
|
||||
{ name: "Sanität & Erstversorgung", reihenfolge: 9 },
|
||||
{ name: "Werkzeug & Räumgerät", reihenfolge: 10 },
|
||||
];
|
||||
273
src/db/seed/data/merkmale.ts
Normal file
273
src/db/seed/data/merkmale.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* Merkmal-Katalog als Seed-Daten (Workstream 9).
|
||||
*
|
||||
* Abgeleitet aus docs/reference/fahrzeug-katalog-noelfv.md, Abschnitt 2.
|
||||
* Genau 34 Merkmale — "Funkrufname" ist eine Spalte auf `vehicles`,
|
||||
* KEIN Merkmal (siehe Plan, Phase 1).
|
||||
*
|
||||
* `slug` ist der Idempotenz-Key (UNIQUE in `merkmale`). Enum-Merkmale tragen
|
||||
* ihre `optionen` (Werte sind die slugartigen DB-Werte, `label` die Anzeige).
|
||||
*/
|
||||
|
||||
export type MerkmalTyp = "number" | "enum" | "boolean" | "text";
|
||||
export type Geltungsbereich = "vehicle" | "equipment" | "both";
|
||||
|
||||
export interface MerkmalOptionSeed {
|
||||
wert: string;
|
||||
label: string;
|
||||
reihenfolge: number;
|
||||
}
|
||||
|
||||
export interface MerkmalSeed {
|
||||
slug: string;
|
||||
name: string;
|
||||
typ: MerkmalTyp;
|
||||
einheit?: string;
|
||||
geltungsbereich: Geltungsbereich;
|
||||
optionen?: MerkmalOptionSeed[];
|
||||
}
|
||||
|
||||
export const MERKMALE: MerkmalSeed[] = [
|
||||
{
|
||||
slug: "loeschwassertank",
|
||||
name: "Löschwassertank",
|
||||
typ: "number",
|
||||
einheit: "l",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "schaummitteltank",
|
||||
name: "Schaummitteltank",
|
||||
typ: "number",
|
||||
einheit: "l",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "schaumzumischung",
|
||||
name: "Schaumzumischung",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "pulverloeschanlage",
|
||||
name: "Pulverlöschanlage",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "pulvermenge",
|
||||
name: "Pulvermenge",
|
||||
typ: "number",
|
||||
einheit: "kg",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "feuerloeschpumpe_typ",
|
||||
name: "Feuerlöschpumpe (Typ)",
|
||||
typ: "enum",
|
||||
geltungsbereich: "vehicle",
|
||||
optionen: [
|
||||
{ wert: "keine", label: "keine", reihenfolge: 0 },
|
||||
{ wert: "fpn_10_750", label: "FPN 10-750", reihenfolge: 1 },
|
||||
{ wert: "fpn_10_1000", label: "FPN 10-1000", reihenfolge: 2 },
|
||||
{ wert: "fpn_10_2000", label: "FPN 10-2000", reihenfolge: 3 },
|
||||
{ wert: "fpn_10_3000", label: "FPN 10-3000", reihenfolge: 4 },
|
||||
{ wert: "fpn_10_6000", label: "FPN 10-6000", reihenfolge: 5 },
|
||||
{ wert: "fph_40_250", label: "FPH 40-250", reihenfolge: 6 },
|
||||
{ wert: "tragkraftspritze", label: "Tragkraftspritze", reihenfolge: 7 },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "pumpen_foerderleistung",
|
||||
name: "Pumpen-Förderleistung",
|
||||
typ: "number",
|
||||
einheit: "l/min",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "schnellangriffseinrichtung",
|
||||
name: "Schnellangriffseinrichtung",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "wasserwerfer",
|
||||
name: "Wasserwerfer",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "wasserwerfer_foerderstrom",
|
||||
name: "Wasserwerfer-Förderstrom",
|
||||
typ: "number",
|
||||
einheit: "l/min",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "besatzung_sitzplaetze",
|
||||
name: "Besatzung / Sitzplätze",
|
||||
typ: "number",
|
||||
einheit: "Plätze",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "zulaessiges_gesamtgewicht",
|
||||
name: "Zulässiges Gesamtgewicht",
|
||||
typ: "number",
|
||||
einheit: "t",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "anzahl_achsen",
|
||||
name: "Anzahl Achsen",
|
||||
typ: "enum",
|
||||
geltungsbereich: "vehicle",
|
||||
optionen: [
|
||||
{ wert: "2", label: "2", reihenfolge: 0 },
|
||||
{ wert: "3", label: "3", reihenfolge: 1 },
|
||||
{ wert: "4", label: "4", reihenfolge: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "allradantrieb",
|
||||
name: "Allradantrieb",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "motorleistung",
|
||||
name: "Motorleistung",
|
||||
typ: "number",
|
||||
einheit: "kW",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "laenge",
|
||||
name: "Länge",
|
||||
typ: "number",
|
||||
einheit: "mm",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "breite",
|
||||
name: "Breite",
|
||||
typ: "number",
|
||||
einheit: "mm",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "hoehe",
|
||||
name: "Höhe",
|
||||
typ: "number",
|
||||
einheit: "mm",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "stromerzeuger_bauart",
|
||||
name: "Stromerzeuger-Bauart",
|
||||
typ: "enum",
|
||||
geltungsbereich: "vehicle",
|
||||
optionen: [
|
||||
{ wert: "keiner", label: "keiner", reihenfolge: 0 },
|
||||
{ wert: "tragbar", label: "tragbar", reihenfolge: 1 },
|
||||
{ wert: "einbaugenerator", label: "Einbaugenerator", reihenfolge: 2 },
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: "stromerzeuger_nennleistung",
|
||||
name: "Stromerzeuger-Nennleistung",
|
||||
typ: "number",
|
||||
einheit: "kVA",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "seilwinde",
|
||||
name: "Seilwinde",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "seilwinden_zugkraft",
|
||||
name: "Seilwinden-Zugkraft",
|
||||
typ: "number",
|
||||
einheit: "kN",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "lichtmast",
|
||||
name: "Lichtmast",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "hydraulischer_rettungssatz",
|
||||
name: "Hydraulischer Rettungssatz",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "both",
|
||||
},
|
||||
{
|
||||
slug: "ladekran",
|
||||
name: "Ladekran",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "kran_hubmoment",
|
||||
name: "Kran-Hubmoment",
|
||||
typ: "number",
|
||||
einheit: "kNm",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "ladebordwand",
|
||||
name: "Ladebordwand",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "ladebordwand_traglast",
|
||||
name: "Ladebordwand-Traglast",
|
||||
typ: "number",
|
||||
einheit: "kg",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "wechselladeeinrichtung",
|
||||
name: "Wechselladeeinrichtung",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "atemluftkompressor_lieferleistung",
|
||||
name: "Atemluftkompressor-Lieferleistung",
|
||||
typ: "number",
|
||||
einheit: "l/min",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "atemluft_speichervolumen",
|
||||
name: "Atemluft-Speichervolumen",
|
||||
typ: "number",
|
||||
einheit: "l",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "anhaengekupplung",
|
||||
name: "Anhängekupplung",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "funkanlage",
|
||||
name: "Funkanlage",
|
||||
typ: "boolean",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
{
|
||||
slug: "baujahr",
|
||||
name: "Baujahr",
|
||||
typ: "number",
|
||||
einheit: "Jahr",
|
||||
geltungsbereich: "vehicle",
|
||||
},
|
||||
];
|
||||
206
src/db/seed/data/vehicle-templates.ts
Normal file
206
src/db/seed/data/vehicle-templates.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Fahrzeug-Vorlagen als Seed-Daten (Workstream 9).
|
||||
*
|
||||
* Abgeleitet aus docs/reference/fahrzeug-katalog-noelfv.md, Abschnitte 1 und 4.
|
||||
* Genau 11 Vorlagen. HLF 4-U ist KEINE eigene Vorlage, sondern ein (offener)
|
||||
* Alias auf HLF 4 mit Pulver-Pflichtmerkmalen.
|
||||
*
|
||||
* Aliasse leben in `vehicle_template_aliasse` mit `bestaetigt`-Flag:
|
||||
* - RLF 2000 / RLFA 2000 (HLF 2) = bestätigt
|
||||
* - RLF 2000-4000 / RLFA 2000-4000 (HLF 3) = bestätigt
|
||||
* - alle anderen = offen (bestaetigt=false)
|
||||
* KEIN HLFA-Alias: Die Allrad-Namensregel ("A" eingeschoben) ist eine
|
||||
* Laufzeitregel im Such-Workstream + das Merkmal `allradantrieb`.
|
||||
*
|
||||
* Vorgabewerte werden typgerecht in genau eine der drei Spalten geschrieben.
|
||||
*/
|
||||
|
||||
export interface AliasSeed {
|
||||
alias: string;
|
||||
bestaetigt: boolean;
|
||||
}
|
||||
|
||||
export interface TemplateMerkmalSeed {
|
||||
slug: string;
|
||||
vorgabewertNum?: number;
|
||||
vorgabewertText?: string;
|
||||
vorgabewertBool?: boolean;
|
||||
pflicht?: boolean;
|
||||
}
|
||||
|
||||
export interface VehicleTemplateSeed {
|
||||
code: string;
|
||||
name: string;
|
||||
beschreibung?: string;
|
||||
aliasse: AliasSeed[];
|
||||
merkmale: TemplateMerkmalSeed[];
|
||||
}
|
||||
|
||||
export const VEHICLE_TEMPLATES: VehicleTemplateSeed[] = [
|
||||
{
|
||||
code: "HLF 1",
|
||||
name: "Hilfeleistungsfahrzeug 1",
|
||||
beschreibung:
|
||||
"Brandbekämpfung/Löschwasserförderung mit Tragkraftspritze plus einfache technische Hilfeleistung (NÖ LFV-RL FA 01).",
|
||||
aliasse: [
|
||||
{ alias: "KLF", bestaetigt: false },
|
||||
{ alias: "KLFA", bestaetigt: false },
|
||||
],
|
||||
merkmale: [
|
||||
{ slug: "feuerloeschpumpe_typ", vorgabewertText: "tragkraftspritze", pflicht: true },
|
||||
{ slug: "allradantrieb", vorgabewertBool: false },
|
||||
{ slug: "funkanlage", vorgabewertBool: true, pflicht: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "HLF 1 W",
|
||||
name: "Hilfeleistungsfahrzeug 1 – Wasser",
|
||||
beschreibung:
|
||||
"Wasserführendes HLF 1 mit Tank, Einbaupumpe und Schnellangriff (NÖ LFV-RL FA 01/W).",
|
||||
aliasse: [
|
||||
{ alias: "KLFA-W", bestaetigt: false },
|
||||
{ alias: "kleines LFA mit Tank", bestaetigt: false },
|
||||
],
|
||||
merkmale: [
|
||||
{ slug: "loeschwassertank", vorgabewertNum: 800, pflicht: true },
|
||||
{ slug: "schnellangriffseinrichtung", vorgabewertBool: true, pflicht: true },
|
||||
{ slug: "allradantrieb", vorgabewertBool: false },
|
||||
{ slug: "funkanlage", vorgabewertBool: true, pflicht: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "HLF 2",
|
||||
name: "Hilfeleistungsfahrzeug 2",
|
||||
beschreibung:
|
||||
"Brandbekämpfung und technische Einsatzleistung, Tank 800–2.000 l (NÖ LFV-RL FA 02).",
|
||||
aliasse: [
|
||||
{ alias: "RLF 2000", bestaetigt: true },
|
||||
{ alias: "RLFA 2000", bestaetigt: true },
|
||||
{ alias: "LF", bestaetigt: false },
|
||||
{ alias: "LFA", bestaetigt: false },
|
||||
{ alias: "TLFA 2000", bestaetigt: false },
|
||||
],
|
||||
merkmale: [
|
||||
{ slug: "feuerloeschpumpe_typ", vorgabewertText: "fpn_10_1000", pflicht: true },
|
||||
{ slug: "loeschwassertank", vorgabewertNum: 2000, pflicht: true },
|
||||
{ slug: "schnellangriffseinrichtung", vorgabewertBool: true, pflicht: true },
|
||||
{ slug: "allradantrieb", vorgabewertBool: false },
|
||||
{ slug: "funkanlage", vorgabewertBool: true, pflicht: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "HLF 3",
|
||||
name: "Hilfeleistungsfahrzeug 3",
|
||||
beschreibung:
|
||||
"Große Brandbekämpfung und technische Einsatzleistung, Tank >2.000–4.000 l (NÖ LFV-RL FA 03).",
|
||||
aliasse: [
|
||||
{ alias: "RLF 2000-4000", bestaetigt: true },
|
||||
{ alias: "RLFA 2000-4000", bestaetigt: true },
|
||||
{ alias: "TLFA 2000-4000", bestaetigt: false },
|
||||
],
|
||||
merkmale: [
|
||||
{ slug: "feuerloeschpumpe_typ", vorgabewertText: "fpn_10_2000", pflicht: true },
|
||||
{ slug: "loeschwassertank", vorgabewertNum: 4000, pflicht: true },
|
||||
{ slug: "wasserwerfer", vorgabewertBool: true, pflicht: true },
|
||||
{ slug: "allradantrieb", vorgabewertBool: false },
|
||||
{ slug: "funkanlage", vorgabewertBool: true, pflicht: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "HLF 4",
|
||||
name: "Hilfeleistungsfahrzeug 4",
|
||||
beschreibung:
|
||||
"Großtanklöschfahrzeug / Wasserversorgung, Tank >5.000–14.000 l (NÖ LFV-RL FA 07). HLF 4-U: Universallöschmittel mit Pulveranlage ≥250 kg.",
|
||||
aliasse: [
|
||||
{ alias: "HLF 4-U", bestaetigt: false },
|
||||
{ alias: "großes TLFA", bestaetigt: false },
|
||||
{ alias: "GTLF", bestaetigt: false },
|
||||
],
|
||||
merkmale: [
|
||||
{ slug: "feuerloeschpumpe_typ", vorgabewertText: "fpn_10_3000", pflicht: true },
|
||||
{ slug: "loeschwassertank", vorgabewertNum: 8000, pflicht: true },
|
||||
{ slug: "wasserwerfer", vorgabewertBool: true, pflicht: true },
|
||||
{ slug: "pulverloeschanlage", vorgabewertBool: true, pflicht: true },
|
||||
{ slug: "pulvermenge", vorgabewertNum: 250 },
|
||||
{ slug: "allradantrieb", vorgabewertBool: false },
|
||||
{ slug: "funkanlage", vorgabewertBool: true, pflicht: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "VRF",
|
||||
name: "Vorausrüstfahrzeug",
|
||||
beschreibung:
|
||||
"Technische Hilfeleistung mit hydraulischem Rettungssatz als Kern (NÖ LFV-RL FA 04).",
|
||||
aliasse: [
|
||||
{ alias: "KRF", bestaetigt: false },
|
||||
{ alias: "KRFA", bestaetigt: false },
|
||||
{ alias: "Vorausrüster", bestaetigt: false },
|
||||
],
|
||||
merkmale: [
|
||||
{ slug: "hydraulischer_rettungssatz", vorgabewertBool: true, pflicht: true },
|
||||
{ slug: "allradantrieb", vorgabewertBool: false },
|
||||
{ slug: "funkanlage", vorgabewertBool: true, pflicht: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "VF",
|
||||
name: "Versorgungs-Logistikfahrzeug",
|
||||
beschreibung:
|
||||
"Transport und Logistik mit Ladebordwand, optional Kran (NÖ LFV-RL FA 06).",
|
||||
aliasse: [
|
||||
{ alias: "LAST", bestaetigt: false },
|
||||
{ alias: "Versorgungsfahrzeug", bestaetigt: false },
|
||||
],
|
||||
merkmale: [
|
||||
{ slug: "ladebordwand", vorgabewertBool: true, pflicht: true },
|
||||
{ slug: "funkanlage", vorgabewertBool: true, pflicht: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "ALF",
|
||||
name: "Atemluftfahrzeug",
|
||||
beschreibung:
|
||||
"Atemluftversorgung und Flaschenfüllung mit Atemluftkompressor (NÖ LFV-RL FA 09).",
|
||||
aliasse: [{ alias: "Atemluftfahrzeug", bestaetigt: false }],
|
||||
merkmale: [
|
||||
{ slug: "atemluftkompressor_lieferleistung", vorgabewertNum: 250, pflicht: true },
|
||||
{ slug: "atemluft_speichervolumen", vorgabewertNum: 30000, pflicht: true },
|
||||
{ slug: "funkanlage", vorgabewertBool: true, pflicht: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "SSTF",
|
||||
name: "Schadstofffahrzeug",
|
||||
beschreibung:
|
||||
"Gefahrgut- und Schadstoffeinsatz: Abdichten, Auffangen, Messen (NÖ LFV-RL FA 10).",
|
||||
aliasse: [
|
||||
{ alias: "Schadstofffahrzeug", bestaetigt: false },
|
||||
{ alias: "Gefahrgutfahrzeug", bestaetigt: false },
|
||||
],
|
||||
merkmale: [
|
||||
{ slug: "funkanlage", vorgabewertBool: true, pflicht: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "WLF",
|
||||
name: "Wechselladerfahrzeug",
|
||||
beschreibung:
|
||||
"Transport von Abrollbehältern, optional Kran/Winde (NÖ LFV-RL FA 05).",
|
||||
aliasse: [{ alias: "WLFA", bestaetigt: false }],
|
||||
merkmale: [
|
||||
{ slug: "wechselladeeinrichtung", vorgabewertBool: true, pflicht: true },
|
||||
{ slug: "funkanlage", vorgabewertBool: true, pflicht: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: "MTF",
|
||||
name: "Mannschaftstransportfahrzeug",
|
||||
beschreibung:
|
||||
"Reiner Mannschaftstransport, 7–14 Sitzplätze, kein Tank/Pumpe (ÖBFV-RL FA 30).",
|
||||
aliasse: [{ alias: "MTFA", bestaetigt: false }],
|
||||
merkmale: [
|
||||
{ slug: "besatzung_sitzplaetze", vorgabewertNum: 9, pflicht: true },
|
||||
{ slug: "funkanlage", vorgabewertBool: true, pflicht: true },
|
||||
],
|
||||
},
|
||||
];
|
||||
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);
|
||||
});
|
||||
}
|
||||
205
src/db/seed/seed.test.ts
Normal file
205
src/db/seed/seed.test.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { MERKMALE } from "./data/merkmale";
|
||||
import { VEHICLE_TEMPLATES } from "./data/vehicle-templates";
|
||||
import { EQUIPMENT_CATEGORIES } from "./data/equipment-categories";
|
||||
|
||||
/**
|
||||
* Reiner Offline-Unit-Test der Seed-DATEN (kein Postgres nötig).
|
||||
*
|
||||
* Prüft die fachlichen Invarianten aus Workstream 9 statisch:
|
||||
* - 34 Merkmale (Funkrufname ist Spalte, NICHT Merkmal)
|
||||
* - 11 Vorlagen, 11 Geräte-Kategorien
|
||||
* - Enum-Optionen: feuerloeschpumpe_typ=8, anzahl_achsen=3, stromerzeuger_bauart=3
|
||||
* - Aliasse mit `bestaetigt`; RLF/RLFA 2000 + 2000-4000 = true
|
||||
* - KEIN HLFA-Alias (Laufzeitregel ist kanonisch)
|
||||
* - HLF 4-U Alias auf HLF 4 + Pulver-Pflichtmerkmale
|
||||
* - Idempotenz-Keys eindeutig (slug, code, name)
|
||||
* - Jeder Template-Merkmal-slug existiert im Merkmal-Katalog
|
||||
* - Vorgabewerte typgerecht zur Merkmal-typ
|
||||
*/
|
||||
|
||||
describe("Seed-Daten: Merkmale", () => {
|
||||
it("enthält genau 34 Merkmale", () => {
|
||||
expect(MERKMALE).toHaveLength(34);
|
||||
});
|
||||
|
||||
it("enthält KEIN Funkrufname-Merkmal (ist Spalte auf vehicles)", () => {
|
||||
const namen = MERKMALE.map((m) => m.name.toLowerCase());
|
||||
expect(namen).not.toContain("funkrufname");
|
||||
const slugs = MERKMALE.map((m) => m.slug);
|
||||
expect(slugs).not.toContain("funkrufname");
|
||||
});
|
||||
|
||||
it("hat eindeutige slugs", () => {
|
||||
const slugs = MERKMALE.map((m) => m.slug);
|
||||
expect(new Set(slugs).size).toBe(slugs.length);
|
||||
});
|
||||
|
||||
it("hat eindeutige Namen", () => {
|
||||
const namen = MERKMALE.map((m) => m.name);
|
||||
expect(new Set(namen).size).toBe(namen.length);
|
||||
});
|
||||
|
||||
it("feuerloeschpumpe_typ hat 8 Enum-Optionen", () => {
|
||||
const m = MERKMALE.find((x) => x.slug === "feuerloeschpumpe_typ");
|
||||
expect(m).toBeDefined();
|
||||
expect(m?.typ).toBe("enum");
|
||||
expect(m?.optionen).toHaveLength(8);
|
||||
});
|
||||
|
||||
it("anzahl_achsen hat 3 Enum-Optionen", () => {
|
||||
const m = MERKMALE.find((x) => x.slug === "anzahl_achsen");
|
||||
expect(m?.optionen).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("stromerzeuger_bauart hat 3 Enum-Optionen", () => {
|
||||
const m = MERKMALE.find((x) => x.slug === "stromerzeuger_bauart");
|
||||
expect(m?.optionen).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("Enum-Merkmale haben Optionen, Nicht-Enum-Merkmale keine", () => {
|
||||
for (const m of MERKMALE) {
|
||||
if (m.typ === "enum") {
|
||||
expect(m.optionen, `${m.slug} braucht Optionen`).toBeDefined();
|
||||
expect((m.optionen ?? []).length).toBeGreaterThan(0);
|
||||
} else {
|
||||
expect(m.optionen ?? [], `${m.slug} darf keine Optionen haben`).toHaveLength(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("jede Enum-Option hat eindeutige Werte je Merkmal", () => {
|
||||
for (const m of MERKMALE) {
|
||||
const werte = (m.optionen ?? []).map((o) => o.wert);
|
||||
expect(new Set(werte).size, `${m.slug} doppelte Optionswerte`).toBe(werte.length);
|
||||
}
|
||||
});
|
||||
|
||||
it("nur erlaubte geltungsbereich-Werte", () => {
|
||||
const erlaubt = new Set(["vehicle", "equipment", "both"]);
|
||||
for (const m of MERKMALE) {
|
||||
expect(erlaubt.has(m.geltungsbereich), `${m.slug}`).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Seed-Daten: Vehicle-Templates", () => {
|
||||
it("enthält genau 11 Vorlagen", () => {
|
||||
expect(VEHICLE_TEMPLATES).toHaveLength(11);
|
||||
});
|
||||
|
||||
it("hat eindeutige codes", () => {
|
||||
const codes = VEHICLE_TEMPLATES.map((t) => t.code);
|
||||
expect(new Set(codes).size).toBe(codes.length);
|
||||
});
|
||||
|
||||
it("enthält KEINE eigene HLFA-Vorlage (Allrad ist Laufzeitregel)", () => {
|
||||
const codes = VEHICLE_TEMPLATES.map((t) => t.code);
|
||||
expect(codes.some((c) => c.startsWith("HLFA"))).toBe(false);
|
||||
});
|
||||
|
||||
it("enthält die 11 erwarteten Codes", () => {
|
||||
const codes = VEHICLE_TEMPLATES.map((t) => t.code).sort();
|
||||
expect(codes).toEqual(
|
||||
["HLF 1", "HLF 1 W", "HLF 2", "HLF 3", "HLF 4", "VRF", "VF", "ALF", "SSTF", "WLF", "MTF"].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it("hat KEINEN HLFA-Alias", () => {
|
||||
const aliasse = VEHICLE_TEMPLATES.flatMap((t) => t.aliasse.map((a) => a.alias));
|
||||
expect(aliasse.some((a) => a.toUpperCase().startsWith("HLFA"))).toBe(false);
|
||||
});
|
||||
|
||||
it("HLF 2: RLF 2000 und RLFA 2000 sind bestätigte Aliasse", () => {
|
||||
const hlf2 = VEHICLE_TEMPLATES.find((t) => t.code === "HLF 2");
|
||||
const byAlias = new Map(hlf2!.aliasse.map((a) => [a.alias, a.bestaetigt]));
|
||||
expect(byAlias.get("RLF 2000")).toBe(true);
|
||||
expect(byAlias.get("RLFA 2000")).toBe(true);
|
||||
});
|
||||
|
||||
it("HLF 3: RLF 2000-4000 und RLFA 2000-4000 sind bestätigte Aliasse", () => {
|
||||
const hlf3 = VEHICLE_TEMPLATES.find((t) => t.code === "HLF 3");
|
||||
const byAlias = new Map(hlf3!.aliasse.map((a) => [a.alias, a.bestaetigt]));
|
||||
expect(byAlias.get("RLF 2000-4000")).toBe(true);
|
||||
expect(byAlias.get("RLFA 2000-4000")).toBe(true);
|
||||
});
|
||||
|
||||
it("HLF 4: HLF 4-U ist ein (offener) Alias", () => {
|
||||
const hlf4 = VEHICLE_TEMPLATES.find((t) => t.code === "HLF 4");
|
||||
const alias = hlf4!.aliasse.find((a) => a.alias === "HLF 4-U");
|
||||
expect(alias).toBeDefined();
|
||||
expect(alias?.bestaetigt).toBe(false);
|
||||
});
|
||||
|
||||
it("HLF 4: pulverloeschanlage ist Pflichtmerkmal", () => {
|
||||
const hlf4 = VEHICLE_TEMPLATES.find((t) => t.code === "HLF 4");
|
||||
const m = hlf4!.merkmale.find((x) => x.slug === "pulverloeschanlage");
|
||||
expect(m).toBeDefined();
|
||||
expect(m?.pflicht).toBe(true);
|
||||
expect(m?.vorgabewertBool).toBe(true);
|
||||
});
|
||||
|
||||
it("nur bestätigte Aliasse sind RLF/RLFA 2000 bzw. 2000-4000", () => {
|
||||
const bestaetigt = VEHICLE_TEMPLATES.flatMap((t) =>
|
||||
t.aliasse.filter((a) => a.bestaetigt).map((a) => a.alias),
|
||||
).sort();
|
||||
expect(bestaetigt).toEqual(
|
||||
["RLF 2000", "RLFA 2000", "RLF 2000-4000", "RLFA 2000-4000"].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
it("alle Aliasse je Vorlage eindeutig", () => {
|
||||
for (const t of VEHICLE_TEMPLATES) {
|
||||
const a = t.aliasse.map((x) => x.alias);
|
||||
expect(new Set(a).size, `${t.code} doppelte Aliasse`).toBe(a.length);
|
||||
}
|
||||
});
|
||||
|
||||
it("jeder Template-Merkmal-slug existiert im Merkmal-Katalog", () => {
|
||||
const known = new Set(MERKMALE.map((m) => m.slug));
|
||||
for (const t of VEHICLE_TEMPLATES) {
|
||||
for (const m of t.merkmale) {
|
||||
expect(known.has(m.slug), `${t.code} -> unbekannter slug ${m.slug}`).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("Vorgabewerte sind typgerecht zum Merkmal-typ gesetzt", () => {
|
||||
const bySlug = new Map(MERKMALE.map((m) => [m.slug, m]));
|
||||
for (const t of VEHICLE_TEMPLATES) {
|
||||
for (const tm of t.merkmale) {
|
||||
const def = bySlug.get(tm.slug)!;
|
||||
if (def.typ === "number" && tm.vorgabewertNum !== undefined) {
|
||||
expect(tm.vorgabewertText, `${t.code}/${tm.slug}`).toBeUndefined();
|
||||
expect(tm.vorgabewertBool, `${t.code}/${tm.slug}`).toBeUndefined();
|
||||
}
|
||||
if (def.typ === "boolean" && tm.vorgabewertBool !== undefined) {
|
||||
expect(tm.vorgabewertNum, `${t.code}/${tm.slug}`).toBeUndefined();
|
||||
expect(tm.vorgabewertText, `${t.code}/${tm.slug}`).toBeUndefined();
|
||||
}
|
||||
if ((def.typ === "enum" || def.typ === "text") && tm.vorgabewertText !== undefined) {
|
||||
expect(tm.vorgabewertNum, `${t.code}/${tm.slug}`).toBeUndefined();
|
||||
expect(tm.vorgabewertBool, `${t.code}/${tm.slug}`).toBeUndefined();
|
||||
}
|
||||
// Enum-Vorgabewert muss eine gültige Option sein
|
||||
if (def.typ === "enum" && tm.vorgabewertText !== undefined) {
|
||||
const werte = (def.optionen ?? []).map((o) => o.wert);
|
||||
expect(werte, `${t.code}/${tm.slug}=${tm.vorgabewertText}`).toContain(
|
||||
tm.vorgabewertText,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Seed-Daten: Equipment-Categories", () => {
|
||||
it("enthält genau 11 Kategorien", () => {
|
||||
expect(EQUIPMENT_CATEGORIES).toHaveLength(11);
|
||||
});
|
||||
|
||||
it("hat eindeutige Namen", () => {
|
||||
const namen = EQUIPMENT_CATEGORIES.map((c) => c.name);
|
||||
expect(new Set(namen).size).toBe(namen.length);
|
||||
});
|
||||
});
|
||||
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