Vollständiges Datenmodell in deutscher Benennung: 20 Tabellen, 9 Aufzählungen, eine Migration, dazu die framework-freien Helfer für tagCodes und Geheimnisse. Regeln stehen zusätzlich in der Datenbank, nicht nur im Service: 12 CHECKs, 2 partielle Eindeutigkeitsindizes, 4 zusammengesetzte Fremdschlüssel. Die Mandantengrenze ist damit strukturell erzwungen. Schrittergebnisse, Kommentare und Protokolleinträge kaskadieren bewusst nicht — das Archiv ist das Gedächtnis der Anwendung. 106 Tests, davon 42 gegen die echten Datenbankregeln. Closes #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
112 lines
4.2 KiB
TypeScript
112 lines
4.2 KiB
TypeScript
import "dotenv/config";
|
||
import { hash } from "@node-rs/argon2";
|
||
import { PrismaPg } from "@prisma/adapter-pg";
|
||
import { PrismaClient } from "../src/generated/prisma/client.js";
|
||
import { Betriebsmodus, Rolle } from "../src/generated/prisma/enums.js";
|
||
|
||
/**
|
||
* Grundbestand, ohne den die Anwendung nicht bedienbar waere: der Betriebsmodus,
|
||
* die eigene Organisation und ein Administrator.
|
||
*
|
||
* Der Seed ist idempotent – zweimal hintereinander aufgerufen aendert der zweite
|
||
* Lauf nichts. Er laeuft im Compose-Stand bewusst NICHT automatisch mit, sondern
|
||
* ist ein dokumentierter Handgriff (siehe README).
|
||
*
|
||
* Die sieben Vorlagen, die Beispiel-Mitarbeiter und die Demo-Kunden kommen mit
|
||
* #017 dazu.
|
||
*/
|
||
|
||
const url = process.env["DATABASE_URL"];
|
||
if (!url) {
|
||
throw new Error("DATABASE_URL ist nicht gesetzt. Siehe .env.example.");
|
||
}
|
||
|
||
const prisma = new PrismaClient({ adapter: new PrismaPg({ connectionString: url }) });
|
||
|
||
/** Akzeptiert den im Auftrag dokumentierten Wert SERVICE als Alias. */
|
||
function modusLesen(): Betriebsmodus {
|
||
const roh = (process.env["SEED_MODE"] ?? "INTERN").trim().toUpperCase();
|
||
if (roh === "DIENSTLEISTUNG" || roh === "SERVICE") return Betriebsmodus.DIENSTLEISTUNG;
|
||
if (roh === "INTERN") return Betriebsmodus.INTERN;
|
||
throw new Error(`SEED_MODE kennt nur INTERN und DIENSTLEISTUNG (Alias SERVICE), nicht "${roh}".`);
|
||
}
|
||
|
||
async function einstellungAnlegen(modus: Betriebsmodus): Promise<void> {
|
||
// Nur anlegen, nicht ueberschreiben: ein spaeterer Seed-Lauf darf einen im
|
||
// Betrieb umgeschalteten Modus nicht zuruecksetzen.
|
||
const vorhanden = await prisma.einstellung.findUnique({ where: { id: "global" } });
|
||
if (vorhanden) {
|
||
console.log(` Einstellung: vorhanden, Modus ${vorhanden.modus} bleibt`);
|
||
return;
|
||
}
|
||
await prisma.einstellung.create({ data: { id: "global", modus } });
|
||
console.log(` Einstellung: angelegt, Modus ${modus}`);
|
||
}
|
||
|
||
async function eigeneOrganisationAnlegen(): Promise<string> {
|
||
const name = process.env["SEED_ORG_NAME"]?.trim() || "Eigene Organisation";
|
||
const kuerzel = (process.env["SEED_ORG_KUERZEL"]?.trim() || "EIGEN").toUpperCase();
|
||
if (!/^[A-Z0-9]{2,6}$/.test(kuerzel)) {
|
||
throw new Error(`SEED_ORG_KUERZEL muss 2 bis 6 Grossbuchstaben oder Ziffern sein, nicht "${kuerzel}".`);
|
||
}
|
||
|
||
// Bewusst nicht aktualisieren: wer Name oder Kuerzel im Betrieb geaendert hat,
|
||
// soll das nicht beim naechsten Seed-Lauf zurueckbekommen. Geaenderte Werte in
|
||
// der Umgebung bleiben deshalb wirkungslos, sobald die Zeile existiert.
|
||
const vorhanden = await prisma.kunde.findFirst({ where: { istEigeneOrganisation: true } });
|
||
if (vorhanden) {
|
||
console.log(` Eigene Organisation: vorhanden (${vorhanden.name})`);
|
||
return vorhanden.id;
|
||
}
|
||
const kunde = await prisma.kunde.create({
|
||
data: { name, kuerzel, istEigeneOrganisation: true },
|
||
});
|
||
console.log(` Eigene Organisation: angelegt (${kunde.name}, ${kunde.kuerzel})`);
|
||
return kunde.id;
|
||
}
|
||
|
||
async function adminAnlegen(): Promise<void> {
|
||
const email = process.env["SEED_ADMIN_EMAIL"]?.trim().toLowerCase();
|
||
const passwort = process.env["SEED_ADMIN_PASSWORD"];
|
||
if (!email || !passwort) {
|
||
console.log(" Administrator: uebersprungen (SEED_ADMIN_EMAIL oder SEED_ADMIN_PASSWORD fehlt)");
|
||
return;
|
||
}
|
||
|
||
// Gesucht wird ueber die Adresse: eine geaenderte SEED_ADMIN_EMAIL legt also
|
||
// einen zweiten Administrator an, statt den ersten umzubenennen. Das ist
|
||
// gewollt – umbenennen waere eine Aenderung an einem Konto, das laengst benutzt
|
||
// sein kann.
|
||
const vorhanden = await prisma.benutzer.findUnique({ where: { email } });
|
||
if (vorhanden) {
|
||
console.log(` Administrator: vorhanden (${email})`);
|
||
return;
|
||
}
|
||
// Das Passwort wird nur als argon2-Hash gespeichert, nie im Klartext.
|
||
await prisma.benutzer.create({
|
||
data: {
|
||
email,
|
||
name: "Administrator",
|
||
rolle: Rolle.ADMIN,
|
||
passwortHash: await hash(passwort),
|
||
aktiv: true,
|
||
},
|
||
});
|
||
console.log(` Administrator: angelegt (${email})`);
|
||
}
|
||
|
||
async function main(): Promise<void> {
|
||
const modus = modusLesen();
|
||
console.log("Seed laeuft:");
|
||
await einstellungAnlegen(modus);
|
||
await eigeneOrganisationAnlegen();
|
||
await adminAnlegen();
|
||
console.log("Seed fertig.");
|
||
}
|
||
|
||
try {
|
||
await main();
|
||
} finally {
|
||
await prisma.$disconnect();
|
||
}
|