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>
219 lines
6.4 KiB
TypeScript
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),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|