feat(seed): mitgelieferte Vorlagen am Schluessel erkennen
Prüfung / pruefung (push) Canceled after 0s
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s

Der Seed erkannte seine sieben Vorlagen am Namen. Wer eine umbenannte, bekam
beim naechsten Lauf eine zweite -- und der Schnellstart stand auf der
Geraeteseite doppelt.

Vorlage.seedSchluessel traegt die Kennung, eindeutig wo gesetzt (partieller
Index, weil eigene Vorlagen keinen tragen). Das Austauschformat kennt das Feld
als seedSchluessel; nicht als schluessel, wie das Issue plante, denn so heisst
dort schon das Feld einer Variablen.

Der Rueckfall auf den Namen bleibt, sonst bekaeme eine Installation von vor
diesem Feld beim ersten Lauf danach genau die Dubletten, die es verhindern
soll. Wird sie so gefunden, traegt der Seed den Schluessel nach -- an der
Entwicklungsdatenbank gefahren: sieben nachgetragen, nichts neu angelegt.

Der Import setzt den Schluessel nicht, auch nicht wenn das Dokument ihn
mitbringt, und der Export schreibt ihn nicht: eine eingespielte Kopie soll
nicht den Platz des Originals einnehmen.

Gegenprobe gefahren: ohne die Suche am Schluessel werden vier Tests rot.

Closes #32
This commit is contained in:
Florian Wilde
2026-10-04 16:48:30 +02:00
parent 88d7db8c3d
commit 7eb81bd191
20 changed files with 316 additions and 36 deletions
+54 -15
View File
@@ -68,11 +68,61 @@ export type GesaeteVorlage = {
readonly neu: boolean;
};
/**
* Sucht die mitgelieferte Vorlage – zuerst am Schluessel, dann am Namen.
*
* Der Schluessel ueberlebt ein Umbenennen, der Name nicht: genau daran legte der
* Seed eine umbenannte Vorlage ein zweites Mal an, und der Schnellstart stand
* auf der Geraeteseite doppelt (#021). Der Rueckfall auf den Namen bleibt, weil
* eine Installation von vor diesem Feld noch keinen Schluessel hat — ohne ihn
* bekaeme sie beim ersten Lauf danach genau die Dubletten, die das Feld
* verhindern soll. Wird sie so gefunden, traegt der Seed den Schluessel nach.
*
* Auch eine **archivierte** Vorlage gilt als vorhanden: eine zweite gleichen
* Namens danebenzustellen waere keine Hilfe, sondern eine Dublette in jeder
* Liste. Wer die Beispielvorlage zurueck will, holt sie aus dem Archiv.
*/
async function mitgelieferteFinden(db: PrismaClient, dokument: Austausch) {
const mitVersion = {
versionen: {
where: { status: VersionStatus.VEROEFFENTLICHT },
orderBy: { version: "desc" as const },
take: 1,
},
};
if (dokument.seedSchluessel !== undefined) {
const amSchluessel = await db.vorlage.findFirst({
where: { seedSchluessel: dokument.seedSchluessel },
include: mitVersion,
});
if (amSchluessel) return amSchluessel;
}
const amNamen = await db.vorlage.findFirst({
// Nur eine Zeile **ohne** Schluessel darf hier zugeordnet werden. Sonst
// koennte eine fremde Vorlage, die zufaellig so heisst wie die mitgelieferte,
// deren Schluessel bekommen - und der naechste Lauf wuerde sie fuer das
// Original halten.
where: { name: dokument.name, kundeId: null, seedSchluessel: null },
include: mitVersion,
});
if (amNamen && dokument.seedSchluessel !== undefined) {
await db.vorlage.update({
where: { id: amNamen.id },
data: { seedSchluessel: dokument.seedSchluessel },
});
console.log(` Vorlage „${amNamen.name}“: Schlüssel ${dokument.seedSchluessel} nachgetragen`);
}
return amNamen;
}
/**
* Legt die Vorlagen als veroeffentlichte Version 1 an.
*
* Idempotent ueber den **Namen**: gibt es die Vorlage schon, bleibt sie
* unangetastet — auch dann, wenn jemand sie inzwischen bearbeitet hat. Ein
* Idempotent ueber den **Schluessel** (Rueckfall: Name, siehe
* `mitgelieferteFinden`): gibt es die Vorlage schon, bleibt sie unangetastet —
* auch dann, wenn jemand sie inzwischen bearbeitet oder umbenannt hat. Ein
* zweiter Seed-Lauf darf die Arbeit eines Redakteurs nicht zurueckdrehen, und
* eine veroeffentlichte Version wird ohnehin nie veraendert.
*/
@@ -82,19 +132,7 @@ export async function vorlagenSaeen(db: PrismaClient): Promise<readonly GesaeteV
let uebersprungen = 0;
for (const { dokument } of vorlagenLesen()) {
const vorhanden = await db.vorlage.findFirst({
// Auch eine **archivierte** Vorlage gilt als vorhanden: eine zweite gleichen
// Namens danebenzustellen waere keine Hilfe, sondern eine Dublette in jeder
// Liste. Wer die Beispielvorlage zurueck will, holt sie aus dem Archiv.
where: { name: dokument.name, kundeId: null },
include: {
versionen: {
where: { status: VersionStatus.VEROEFFENTLICHT },
orderBy: { version: "desc" },
take: 1,
},
},
});
const vorhanden = await mitgelieferteFinden(db, dokument);
const veroeffentlichte = vorhanden?.versionen[0];
if (vorhanden && veroeffentlichte) {
ergebnis.push({
@@ -131,6 +169,7 @@ export async function vorlagenSaeen(db: PrismaClient): Promise<readonly GesaeteV
beschreibung: dokument.beschreibung ?? null,
kategorie: dokument.kategorie ?? null,
schlagworte: [...dokument.schlagworte],
seedSchluessel: dokument.seedSchluessel ?? null,
},
}));
@@ -1,6 +1,7 @@
{
"format": 1,
"name": "PC Neuinstallation",
"seedSchluessel": "pc-neuinstallation",
"beschreibung": "Vollständige Neuinstallation eines Arbeitsplatzrechners: von der Datensicherung des alten Systems über Installation, Einbindung und Verschlüsselung bis zur Übergabe an den Benutzer.",
"kategorie": "Client",
"schlagworte": ["windows", "client", "neuinstallation", "bitlocker"],
@@ -1,6 +1,7 @@
{
"format": 1,
"name": "Mitarbeiter-Onboarding",
"seedSchluessel": "mitarbeiter-onboarding",
"beschreibung": "Neues Benutzerkonto im Active Directory oder in Entra ID anlegen, Hardware bereitstellen und den ersten Arbeitstag begleiten.",
"kategorie": "Benutzer",
"schlagworte": ["onboarding", "eintritt", "benutzerkonto", "active directory", "entra id"],
@@ -1,6 +1,7 @@
{
"format": 1,
"name": "Mitarbeiter-Offboarding",
"seedSchluessel": "mitarbeiter-offboarding",
"beschreibung": "Konto sperren, Postfach und Daten übergeben, Hardware und Zugänge einsammeln, wenn jemand das Haus verlässt.",
"kategorie": "Benutzer",
"schlagworte": ["offboarding", "austritt", "benutzerkonto", "sperrung"],
@@ -1,6 +1,7 @@
{
"format": 1,
"name": "Server-Inbetriebnahme Windows-VM",
"seedSchluessel": "server-inbetriebnahme",
"beschreibung": "Inbetriebnahme einer neuen Windows-Server-VM: Planung und Adressvergabe, Installation, Domänenbeitritt, Absicherung sowie Aufnahme in Backup und Monitoring.",
"kategorie": "Server",
"schlagworte": ["windows", "server", "vm", "inbetriebnahme"],
@@ -1,6 +1,7 @@
{
"format": 1,
"name": "Firewall-Rollout OPNsense",
"seedSchluessel": "firewall-rollout-opnsense",
"beschreibung": "Austausch oder Neuaufbau einer OPNsense-Firewall: von den Providerdaten über Regelwerk und VPN bis zur Abnahme und zur außer Betrieb genommenen Altfirewall.",
"kategorie": "Netzwerk",
"schlagworte": ["firewall", "opnsense", "netzwerk", "vpn", "rollout"],
@@ -1,6 +1,7 @@
{
"format": 1,
"name": "Monatlicher Patchday",
"seedSchluessel": "monatlicher-patchday",
"beschreibung": "Der wiederkehrende Ablauf für Updates an Servern, Hypervisor, Netzgeräten und Clients: ankündigen, einspielen, kontrollieren und berichten.",
"kategorie": "Wartung",
"schlagworte": ["patchday", "updates", "wartung", "monatlich"],
@@ -1,6 +1,7 @@
{
"format": 1,
"name": "Backup-Restore-Test",
"seedSchluessel": "backup-restore-test",
"beschreibung": "Der regelmäßige Nachweis, dass sich eine Sicherung wirklich zurückholen lässt: Wiederherstellungspunkt prüfen, in einer isolierten Umgebung zurückspielen, Funktion und Dauer festhalten.",
"kategorie": "Wartung",
"schlagworte": ["backup", "restore", "wiederherstellung", "test", "wartung"],