Files
Florian-netz/src/db/seed/upsert.ts
Matthias Hochmeister f99c1f1abd fix(seed): tsx-Laufzeit-Imports im Katalog-Seed auf relative .js umstellen
Der Standalone-Runner `tsx src/db/seed/index.ts` (npm run db:seed)
importierte Laufzeit-Module ueber den tsconfig-Pfad-Alias `@/`
(`@/db/schema`, `@/lib/audit`). tsx loest `paths` nur versionsabhaengig
auf (erst ab v4.20); package.json pinnt jedoch `tsx ^4.19.2`, sodass
eine 4.19.x-Aufloesung mit ERR_MODULE_NOT_FOUND scheitert, bevor eine
DB-Verbindung aufgebaut wird. Der Offline-Unit-Test maskierte das, weil
er nur die `./data/*`-Dateien importiert und Vite den Alias aufloest.

Fix: index.ts und upsert.ts nutzen jetzt relative `.js`-Imports
(`../schema/index.js`, `../../lib/audit.js`, `./data/*.js`, `./upsert.js`)
analog zu scripts/seed-auth.ts und scripts/migrate.ts. Damit ist die
Aufloesung tsx-versionsunabhaengig und konsistent zur etablierten
Konvention der uebrigen per tsx ausgefuehrten Scripts.

Verifiziert offline: tsc --noEmit (0), 25 Seed-Unit-Tests gruen, und
das Seed-Modul-Importgraph laedt unter `node --import tsx/esm` ohne
Resolver-Fehler. Die tatsaechliche `npm run db:seed`-Ausfuehrung gegen
Postgres bleibt deferred (kein Postgres/Server im Sandbox).

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

219 lines
6.4 KiB
TypeScript

import { eq, and } from "drizzle-orm";
import type { Tx } from "../../lib/audit.js";
import * as schema from "../schema/index.js";
import type { MerkmalSeed } from "./data/merkmale.js";
import type { VehicleTemplateSeed } from "./data/vehicle-templates.js";
import type { EquipmentCategorySeed } from "./data/equipment-categories.js";
/**
* 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),
),
);
}
}
}