feat(seed): mitgelieferte Vorlagen am Schluessel erkennen #42
@@ -24,6 +24,12 @@ und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
|||||||
|
|
||||||
### Hinzugefügt
|
### Hinzugefügt
|
||||||
|
|
||||||
|
- Mitgelieferte Vorlagen tragen einen **stabilen Schlüssel** (`seedSchluessel`). Der Seed erkennt
|
||||||
|
seine sieben Vorlagen daran statt am Namen — wer eine umbenannte, bekam vorher beim nächsten Lauf
|
||||||
|
eine zweite, samt doppeltem Schnellstart auf der Geräteseite. Eine bestehende Installation bekommt
|
||||||
|
den Schlüssel beim ersten Lauf danach nachgetragen, ohne Dubletten. Eine über den Import
|
||||||
|
eingespielte Kopie bekommt **keinen** Schlüssel: sie soll nicht den Platz des Originals einnehmen
|
||||||
|
(#021)
|
||||||
- `pnpm pruefe:doku` vergleicht die Schranken-Befehle in `CLAUDE.md` und `README.md` und verlangt von
|
- `pnpm pruefe:doku` vergleicht die Schranken-Befehle in `CLAUDE.md` und `README.md` und verlangt von
|
||||||
beiden den LDAP-Container. Genau diese Abweichung war unbemerkt entstanden (#040)
|
beiden den LDAP-Container. Genau diese Abweichung war unbemerkt entstanden (#040)
|
||||||
- Fünf Tests für den Healthcheck, darunter einer, der belegt, dass die Begründung ins Protokoll
|
- Fünf Tests für den Healthcheck, darunter einer, der belegt, dass die Begründung ins Protokoll
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# #021 Stabiler Schlüssel für Seed-Vorlagen
|
# #021 Stabiler Schlüssel für Seed-Vorlagen
|
||||||
|
|
||||||
**Status:** offen
|
**Status:** erledigt
|
||||||
**Im Tracker:** [#32](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/32) — die Zählung dort schließt Pull Requests ein und läuft deshalb auseinander
|
**Im Tracker:** [#32](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/32) — die Zählung dort schließt Pull Requests ein und läuft deshalb auseinander
|
||||||
**Meilenstein:** v0.2.0
|
**Meilenstein:** v0.2.0
|
||||||
**Labels:** phase-2, backend
|
**Labels:** phase-2, backend
|
||||||
|
|
||||||
**Geschätzt:** 2 h **Tatsächlich:** –
|
**Geschätzt:** 2 h **Tatsächlich:** 55 min
|
||||||
|
|
||||||
## Ziel
|
## Ziel
|
||||||
|
|
||||||
@@ -15,17 +15,19 @@ Geräteseite doppelt. Aufgefallen im Review zu [#017](017-seed-sieben-vorlagen.m
|
|||||||
|
|
||||||
## Aufgaben
|
## Aufgaben
|
||||||
|
|
||||||
- [ ] Ein optionales Feld `schluessel` im [Austauschformat](../vorlagen-format.md) — gedacht für
|
- [x] Ein optionales Feld `seedSchluessel` im [Austauschformat](../vorlagen-format.md) — **nicht**
|
||||||
|
`schluessel` wie hier geplant: so heißt im Format schon das Feld einer Variablen, und zwei
|
||||||
|
Dinge gleichen Namens in derselben Datei wären eine Lesefalle — gedacht für
|
||||||
mitgelieferte Vorlagen, nicht für die Oberfläche; beim Import **nicht** übernommen, damit eine
|
mitgelieferte Vorlagen, nicht für die Oberfläche; beim Import **nicht** übernommen, damit eine
|
||||||
eingespielte Kopie nicht den Platz des Originals einnimmt
|
eingespielte Kopie nicht den Platz des Originals einnimmt
|
||||||
- [ ] `Vorlage.seedSchluessel` mit partiellem eindeutigen Index (nur wo gesetzt)
|
- [x] `Vorlage.seedSchluessel` mit partiellem eindeutigen Index (nur wo gesetzt)
|
||||||
- [ ] Der Seed sucht über den Schlüssel und fällt auf den Namen zurück, solange noch keiner gesetzt
|
- [x] Der Seed sucht über den Schlüssel und fällt auf den Namen zurück, solange noch keiner gesetzt
|
||||||
ist — eine bestehende Installation darf beim ersten Lauf danach keine Dubletten bekommen
|
ist — eine bestehende Installation darf beim ersten Lauf danach keine Dubletten bekommen
|
||||||
- [ ] Dokumentieren, was der Schlüssel **nicht** tut: er macht eine Vorlage nicht unveränderlich und
|
- [x] Dokumentieren, was der Schlüssel **nicht** tut: er macht eine Vorlage nicht unveränderlich und
|
||||||
erlaubt dem Seed nicht, eine bearbeitete Vorlage zu überschreiben
|
erlaubt dem Seed nicht, eine bearbeitete Vorlage zu überschreiben
|
||||||
|
|
||||||
## Akzeptanzkriterien
|
## Akzeptanzkriterien
|
||||||
|
|
||||||
- [ ] Test: umbenannte Seed-Vorlage, zweiter Seed-Lauf legt keine zweite an
|
- [x] Test: umbenannte Seed-Vorlage, zweiter Seed-Lauf legt keine zweite an
|
||||||
- [ ] Test: zweiter Lauf erzeugt keine zweite `GeraetetypVorlage`-Zeile
|
- [x] Test: zweiter Lauf erzeugt keine zweite `GeraetetypVorlage`-Zeile
|
||||||
- [ ] Test: eine über den Import eingespielte Kopie bekommt **keinen** Schlüssel
|
- [x] Test: eine über den Import eingespielte Kopie bekommt **keinen** Schlüssel
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# #027 Die Editor-Abläufe lassen ihre Vorlagen stehen
|
||||||
|
|
||||||
|
**Status:** offen
|
||||||
|
**Im Tracker:** –
|
||||||
|
**Meilenstein:** v0.2.0
|
||||||
|
**Labels:** phase-2, test
|
||||||
|
**Geschätzt:** 1 h **Tatsächlich:** –
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
In der Entwicklungsdatenbank stehen über hundert Vorlagen mit Namen wie
|
||||||
|
`ZZ E2E Editor 1790324192596 C`. Die Ende-zu-Ende-Abläufe legen sie an und räumen sie nicht weg; bei
|
||||||
|
jedem Lauf kommen drei dazu.
|
||||||
|
|
||||||
|
Aufgefallen beim Nachsehen zu [#021](021-stabiler-schluessel-vorlagen.md), als eine Abfrage über
|
||||||
|
`Vorlage` statt sieben Zeilen über hundert lieferte.
|
||||||
|
|
||||||
|
Das ist kein Schaden an den Tests — das `ZZ`-Präfix sortiert sie ans Ende, und jeder Ablauf legt
|
||||||
|
seine eigenen an. Es ist ein Schaden an allem, was **danach** in dieselbe Datenbank sieht: die
|
||||||
|
Vorlagenliste in der Oberfläche, Screenshots, und jede Abfrage von Hand. Bei einem Fehler, der
|
||||||
|
„0 Treffer“ oder „zu viele Treffer“ heißt, kostet das Zeit.
|
||||||
|
|
||||||
|
## Aufgaben
|
||||||
|
|
||||||
|
- [ ] Die Abläufe unter `tests/e2e/`, die Vorlagen anlegen, räumen sie in einem Nachlauf wieder weg —
|
||||||
|
über ihr Präfix, nicht über gemerkte Kennungen (ein abgebrochener Lauf merkt sich nichts)
|
||||||
|
- [ ] Dasselbe für Geräte, Personen und Läufe prüfen, die die Abläufe anlegen
|
||||||
|
- [ ] Einmalig aufräumen: die vorhandenen Reste aus der Entwicklungsdatenbank entfernen
|
||||||
|
- [ ] Prüfen, ob ein Nachlauf das überhaupt leisten kann — ein Ablauf, der mitten im Lauf scheitert,
|
||||||
|
darf den Nachlauf nicht überspringen
|
||||||
|
|
||||||
|
## Akzeptanzkriterien
|
||||||
|
|
||||||
|
- [ ] Nach `pnpm test:e2e` steht in `Vorlage` keine Zeile mit `ZZ E2E`
|
||||||
|
- [ ] Ein abgebrochener Lauf lässt ebenfalls nichts stehen
|
||||||
@@ -25,10 +25,11 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
|
|||||||
| #018 | [M13 – E2E-Tests, Anleitung, PDF](018-e2e-anleitung-pdf.md) | erledigt | v0.1.0 | 14 h | 2 h 15 min |
|
| #018 | [M13 – E2E-Tests, Anleitung, PDF](018-e2e-anleitung-pdf.md) | erledigt | v0.1.0 | 14 h | 2 h 15 min |
|
||||||
| #019 | [M14 – Betriebswege und Release v0.1.0](019-betrieb-release.md) | erledigt | v0.1.0 | 10 h | 5 h 25 min |
|
| #019 | [M14 – Betriebswege und Release v0.1.0](019-betrieb-release.md) | erledigt | v0.1.0 | 10 h | 5 h 25 min |
|
||||||
| #020 | [Vorgesetzter auch für von Hand angelegte Personen](020-vorgesetzter-manuell.md) | offen | v0.2.0 | 3 h | – |
|
| #020 | [Vorgesetzter auch für von Hand angelegte Personen](020-vorgesetzter-manuell.md) | offen | v0.2.0 | 3 h | – |
|
||||||
| #021 | [Stabiler Schlüssel für Seed-Vorlagen](021-stabiler-schluessel-vorlagen.md) | offen | v0.2.0 | 2 h | – |
|
| #021 | [Stabiler Schlüssel für Seed-Vorlagen](021-stabiler-schluessel-vorlagen.md) | erledigt | v0.2.0 | 2 h | 55 min |
|
||||||
| #022 | [Schnellstart je Gerätetyp pflegen](022-schnellstart-pflegen.md) | offen | v0.2.0 | 3 h | – |
|
| #022 | [Schnellstart je Gerätetyp pflegen](022-schnellstart-pflegen.md) | offen | v0.2.0 | 3 h | – |
|
||||||
| #023 | [Protokoll ansehen](023-protokoll-ansicht.md) | offen | v0.2.0 | 4 h | – |
|
| #023 | [Protokoll ansehen](023-protokoll-ansicht.md) | offen | v0.2.0 | 4 h | – |
|
||||||
| #024 | [Proxmox-Einzeiler auf einem echten PVE-Host gegenprüfen](024-proxmox-echtlauf.md) | offen | v0.2.0 | 2 h | – |
|
| #024 | [Proxmox-Einzeiler auf einem echten PVE-Host gegenprüfen](024-proxmox-echtlauf.md) | offen | v0.2.0 | 2 h | – |
|
||||||
| #025 | [Healthcheck gibt die rohe Datenbankfehlermeldung heraus](025-gesundheit-fehlermeldung.md) | erledigt | v0.2.0 | 1 h | 25 min |
|
| #025 | [Healthcheck gibt die rohe Datenbankfehlermeldung heraus](025-gesundheit-fehlermeldung.md) | erledigt | v0.2.0 | 1 h | 25 min |
|
||||||
| #026 | [Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind](026-konventionen-praezisieren.md) | erledigt | v0.2.0 | 1 h | 20 min |
|
| #026 | [Zwei Konventionen präzisieren, die strenger klingen als sie gemeint sind](026-konventionen-praezisieren.md) | erledigt | v0.2.0 | 1 h | 20 min |
|
||||||
| | **Summe** | | | **152 h** | **36 h 05 min** |
|
| #027 | [Die Editor-Abläufe lassen ihre Vorlagen stehen](027-e2e-reste-vorlagen.md) | offen | v0.2.0 | 1 h | – |
|
||||||
|
| | **Summe** | | | **153 h** | **37 h 00 min** |
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ Platzhaltern. **JSON kennt keine Kommentare** — die Zeilen mit `//` müssen vo
|
|||||||
| -------------- | -------- | ------------------------------------------------------------------------------- |
|
| -------------- | -------- | ------------------------------------------------------------------------------- |
|
||||||
| `format` | Pflicht | Fassung des Austauschformats. Derzeit `1`; ein anderer Wert wird abgewiesen. |
|
| `format` | Pflicht | Fassung des Austauschformats. Derzeit `1`; ein anderer Wert wird abgewiesen. |
|
||||||
| `name` | Pflicht | Name der Vorlage, nicht leer. Namen sind **nicht** eindeutig. |
|
| `name` | Pflicht | Name der Vorlage, nicht leer. Namen sind **nicht** eindeutig. |
|
||||||
|
| `seedSchluessel` | optional | **Nur für mitgelieferte Vorlagen.** Siehe unten — beim Import verfällt das Feld. |
|
||||||
| `beschreibung` | optional | Ein bis zwei Sätze, was die Vorlage abdeckt. |
|
| `beschreibung` | optional | Ein bis zwei Sätze, was die Vorlage abdeckt. |
|
||||||
| `kategorie` | optional | Freitext, gruppiert die Vorlagenliste (etwa `Onboarding`). |
|
| `kategorie` | optional | Freitext, gruppiert die Vorlagenliste (etwa `Onboarding`). |
|
||||||
| `schlagworte` | optional | Liste nicht leerer Texte; Standard `[]`. |
|
| `schlagworte` | optional | Liste nicht leerer Texte; Standard `[]`. |
|
||||||
@@ -230,3 +231,33 @@ Das Feld `format` ist die Fassung des Formats, nicht die der Anwendung. Es gilt:
|
|||||||
der die Hälfte versteht und die andere Hälfte still verwirft, wäre schlimmer als ein Fehlschlag.
|
der die Hälfte versteht und die andere Hälfte still verwirft, wäre schlimmer als ein Fehlschlag.
|
||||||
|
|
||||||
Die Zahl steht als `FORMAT_VERSION` in `src/lib/vorlagen-format.ts` an genau einer Stelle.
|
Die Zahl steht als `FORMAT_VERSION` in `src/lib/vorlagen-format.ts` an genau einer Stelle.
|
||||||
|
|
||||||
|
## `seedSchluessel` — nur für mitgelieferte Vorlagen
|
||||||
|
|
||||||
|
Die sieben Vorlagen, die Laufzettel mitbringt, tragen in ihrer Datei ein zusätzliches Feld:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "format": 1, "name": "PC Neuinstallation", "seedSchluessel": "pc-neuinstallation" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Daran erkennt der Seed sie wieder. Vorher ging das über den **Namen** — und wer eine mitgelieferte
|
||||||
|
Vorlage umbenannte, bekam beim nächsten Seed-Lauf eine zweite, samt doppeltem Schnellstart auf der
|
||||||
|
Geräteseite (#021).
|
||||||
|
|
||||||
|
**Was der Schlüssel nicht tut:**
|
||||||
|
|
||||||
|
- Er macht eine Vorlage **nicht unveränderlich.** Umbenennen, umbauen, archivieren — alles bleibt
|
||||||
|
erlaubt. Der Seed erkennt sie danach wieder und lässt sie in Ruhe.
|
||||||
|
- Er erlaubt dem Seed **nicht**, eine bearbeitete Vorlage zu überschreiben. Gibt es die Vorlage, legt
|
||||||
|
er nichts an und ändert nichts. Das war vorher so und bleibt so.
|
||||||
|
- Er ist **keine Kennung über Installationen hinweg.** Zwei Installationen haben für dieselbe
|
||||||
|
mitgelieferte Vorlage denselben Schlüssel, aber verschiedene `id`s — der Schlüssel sagt nur
|
||||||
|
„diese Zeile stammt aus dem Seed“.
|
||||||
|
|
||||||
|
**Beim Import verfällt das Feld.** Wer eine mitgelieferte Vorlage exportiert und wieder einspielt,
|
||||||
|
bekommt eine gewöhnliche Vorlage ohne Schlüssel. Das ist Absicht: die Kopie soll nicht den Platz des
|
||||||
|
Originals einnehmen, sonst hielte der Seed sie dafür und würde das Original ein zweites Mal anlegen.
|
||||||
|
Der Export schreibt das Feld deshalb auch nicht — ein Feld, das beim Einlesen verfällt, verwirrt nur.
|
||||||
|
|
||||||
|
Eigene Vorlagen brauchen den Schlüssel nicht und sollten ihn nicht bekommen. Er ist eindeutig, wo er
|
||||||
|
gesetzt ist; eine zweite Vorlage mit demselben Schlüssel weist die Datenbank ab.
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- HANDGESCHRIEBEN. Prisma erzeugt hier zusaetzlich Loeschbefehle fuer die
|
||||||
|
-- zusammengesetzten Fremdschluessel (Mandantengrenze), die Trigramm-Indizes
|
||||||
|
-- (Archivsuche), die partiellen eindeutigen Indizes und die Trigger auf
|
||||||
|
-- veroeffentlichten Versionen: es kennt sie nicht, weil sie sich im Schema nicht
|
||||||
|
-- ausdruecken lassen. Diese Loeschbefehle sind bewusst gestrichen.
|
||||||
|
--
|
||||||
|
-- tests/datenbankregeln.test.ts prueft, dass alle diese Objekte vorhanden sind –
|
||||||
|
-- ein versehentlich stehengelassener Loeschbefehl faellt dort sofort auf.
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- Stabiler Schluessel fuer mitgelieferte Vorlagen (#021).
|
||||||
|
--
|
||||||
|
-- Bisher erkannte der Seed seine sieben Vorlagen am Namen. Wurde eine
|
||||||
|
-- umbenannt, legte der naechste Lauf sie erneut an, und der Schnellstart stand
|
||||||
|
-- auf der Geraeteseite doppelt. Der Schluessel ueberlebt das Umbenennen.
|
||||||
|
ALTER TABLE "Vorlage"
|
||||||
|
ADD COLUMN "seedSchluessel" TEXT;
|
||||||
|
|
||||||
|
-- Eindeutig nur dort, wo gesetzt: eigene Vorlagen tragen keinen Schluessel, und
|
||||||
|
-- ein gewoehnlicher eindeutiger Index wuerde an der zweiten davon scheitern.
|
||||||
|
CREATE UNIQUE INDEX "Vorlage_seedSchluessel_einmalig"
|
||||||
|
ON "Vorlage" ("seedSchluessel")
|
||||||
|
WHERE "seedSchluessel" IS NOT NULL;
|
||||||
+16
-9
@@ -368,17 +368,24 @@ model GeraetetypVorlage {
|
|||||||
// ---- Vorlagen --------------------------------------------------------------
|
// ---- Vorlagen --------------------------------------------------------------
|
||||||
|
|
||||||
model Vorlage {
|
model Vorlage {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
beschreibung String?
|
beschreibung String?
|
||||||
kategorie String?
|
kategorie String?
|
||||||
schlagworte String[] @default([])
|
schlagworte String[] @default([])
|
||||||
/// Nur im Modus DIENSTLEISTUNG: kundenspezifische Vorlage. Leer heisst
|
/// Nur im Modus DIENSTLEISTUNG: kundenspezifische Vorlage. Leer heisst
|
||||||
/// "gilt fuer alle".
|
/// "gilt fuer alle".
|
||||||
kundeId String?
|
kundeId String?
|
||||||
archiviert Boolean @default(false)
|
archiviert Boolean @default(false)
|
||||||
erstelltAm DateTime @default(now())
|
/// Kennung einer **mitgelieferten** Vorlage. Der Seed setzt sie und erkennt
|
||||||
geaendertAm DateTime @default(now()) @updatedAt
|
/// seine sieben Vorlagen daran statt am Namen, der sich aendern darf (#021).
|
||||||
|
/// Eindeutig, wo gesetzt — erzwungen durch den partiellen eindeutigen Index
|
||||||
|
/// `Vorlage_seedSchluessel_einmalig`, den Prisma nicht ausdruecken kann.
|
||||||
|
/// Der Import setzt sie absichtlich **nicht**: eine eingespielte Kopie soll
|
||||||
|
/// nicht den Platz des Originals einnehmen.
|
||||||
|
seedSchluessel String?
|
||||||
|
erstelltAm DateTime @default(now())
|
||||||
|
geaendertAm DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
kunde Kunde? @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
kunde Kunde? @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||||||
versionen VorlagenVersion[]
|
versionen VorlagenVersion[]
|
||||||
|
|||||||
@@ -68,11 +68,61 @@ export type GesaeteVorlage = {
|
|||||||
readonly neu: boolean;
|
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.
|
* Legt die Vorlagen als veroeffentlichte Version 1 an.
|
||||||
*
|
*
|
||||||
* Idempotent ueber den **Namen**: gibt es die Vorlage schon, bleibt sie
|
* Idempotent ueber den **Schluessel** (Rueckfall: Name, siehe
|
||||||
* unangetastet — auch dann, wenn jemand sie inzwischen bearbeitet hat. Ein
|
* `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
|
* zweiter Seed-Lauf darf die Arbeit eines Redakteurs nicht zurueckdrehen, und
|
||||||
* eine veroeffentlichte Version wird ohnehin nie veraendert.
|
* eine veroeffentlichte Version wird ohnehin nie veraendert.
|
||||||
*/
|
*/
|
||||||
@@ -82,19 +132,7 @@ export async function vorlagenSaeen(db: PrismaClient): Promise<readonly GesaeteV
|
|||||||
let uebersprungen = 0;
|
let uebersprungen = 0;
|
||||||
|
|
||||||
for (const { dokument } of vorlagenLesen()) {
|
for (const { dokument } of vorlagenLesen()) {
|
||||||
const vorhanden = await db.vorlage.findFirst({
|
const vorhanden = await mitgelieferteFinden(db, dokument);
|
||||||
// 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 veroeffentlichte = vorhanden?.versionen[0];
|
const veroeffentlichte = vorhanden?.versionen[0];
|
||||||
if (vorhanden && veroeffentlichte) {
|
if (vorhanden && veroeffentlichte) {
|
||||||
ergebnis.push({
|
ergebnis.push({
|
||||||
@@ -131,6 +169,7 @@ export async function vorlagenSaeen(db: PrismaClient): Promise<readonly GesaeteV
|
|||||||
beschreibung: dokument.beschreibung ?? null,
|
beschreibung: dokument.beschreibung ?? null,
|
||||||
kategorie: dokument.kategorie ?? null,
|
kategorie: dokument.kategorie ?? null,
|
||||||
schlagworte: [...dokument.schlagworte],
|
schlagworte: [...dokument.schlagworte],
|
||||||
|
seedSchluessel: dokument.seedSchluessel ?? null,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"format": 1,
|
"format": 1,
|
||||||
"name": "PC Neuinstallation",
|
"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.",
|
"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",
|
"kategorie": "Client",
|
||||||
"schlagworte": ["windows", "client", "neuinstallation", "bitlocker"],
|
"schlagworte": ["windows", "client", "neuinstallation", "bitlocker"],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"format": 1,
|
"format": 1,
|
||||||
"name": "Mitarbeiter-Onboarding",
|
"name": "Mitarbeiter-Onboarding",
|
||||||
|
"seedSchluessel": "mitarbeiter-onboarding",
|
||||||
"beschreibung": "Neues Benutzerkonto im Active Directory oder in Entra ID anlegen, Hardware bereitstellen und den ersten Arbeitstag begleiten.",
|
"beschreibung": "Neues Benutzerkonto im Active Directory oder in Entra ID anlegen, Hardware bereitstellen und den ersten Arbeitstag begleiten.",
|
||||||
"kategorie": "Benutzer",
|
"kategorie": "Benutzer",
|
||||||
"schlagworte": ["onboarding", "eintritt", "benutzerkonto", "active directory", "entra id"],
|
"schlagworte": ["onboarding", "eintritt", "benutzerkonto", "active directory", "entra id"],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"format": 1,
|
"format": 1,
|
||||||
"name": "Mitarbeiter-Offboarding",
|
"name": "Mitarbeiter-Offboarding",
|
||||||
|
"seedSchluessel": "mitarbeiter-offboarding",
|
||||||
"beschreibung": "Konto sperren, Postfach und Daten übergeben, Hardware und Zugänge einsammeln, wenn jemand das Haus verlässt.",
|
"beschreibung": "Konto sperren, Postfach und Daten übergeben, Hardware und Zugänge einsammeln, wenn jemand das Haus verlässt.",
|
||||||
"kategorie": "Benutzer",
|
"kategorie": "Benutzer",
|
||||||
"schlagworte": ["offboarding", "austritt", "benutzerkonto", "sperrung"],
|
"schlagworte": ["offboarding", "austritt", "benutzerkonto", "sperrung"],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"format": 1,
|
"format": 1,
|
||||||
"name": "Server-Inbetriebnahme Windows-VM",
|
"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.",
|
"beschreibung": "Inbetriebnahme einer neuen Windows-Server-VM: Planung und Adressvergabe, Installation, Domänenbeitritt, Absicherung sowie Aufnahme in Backup und Monitoring.",
|
||||||
"kategorie": "Server",
|
"kategorie": "Server",
|
||||||
"schlagworte": ["windows", "server", "vm", "inbetriebnahme"],
|
"schlagworte": ["windows", "server", "vm", "inbetriebnahme"],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"format": 1,
|
"format": 1,
|
||||||
"name": "Firewall-Rollout OPNsense",
|
"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.",
|
"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",
|
"kategorie": "Netzwerk",
|
||||||
"schlagworte": ["firewall", "opnsense", "netzwerk", "vpn", "rollout"],
|
"schlagworte": ["firewall", "opnsense", "netzwerk", "vpn", "rollout"],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"format": 1,
|
"format": 1,
|
||||||
"name": "Monatlicher Patchday",
|
"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.",
|
"beschreibung": "Der wiederkehrende Ablauf für Updates an Servern, Hypervisor, Netzgeräten und Clients: ankündigen, einspielen, kontrollieren und berichten.",
|
||||||
"kategorie": "Wartung",
|
"kategorie": "Wartung",
|
||||||
"schlagworte": ["patchday", "updates", "wartung", "monatlich"],
|
"schlagworte": ["patchday", "updates", "wartung", "monatlich"],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"format": 1,
|
"format": 1,
|
||||||
"name": "Backup-Restore-Test",
|
"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.",
|
"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",
|
"kategorie": "Wartung",
|
||||||
"schlagworte": ["backup", "restore", "wiederherstellung", "test", "wartung"],
|
"schlagworte": ["backup", "restore", "wiederherstellung", "test", "wartung"],
|
||||||
|
|||||||
@@ -175,6 +175,25 @@ export const AustauschSchema = z.object({
|
|||||||
message: `Unbekanntes Format. Diese Fassung liest Format ${FORMAT_VERSION}.`,
|
message: `Unbekanntes Format. Diese Fassung liest Format ${FORMAT_VERSION}.`,
|
||||||
}),
|
}),
|
||||||
name: z.string().trim().min(1, "Die Vorlage braucht einen Namen.").max(200),
|
name: z.string().trim().min(1, "Die Vorlage braucht einen Namen.").max(200),
|
||||||
|
/**
|
||||||
|
* Kennung einer **mitgelieferten** Vorlage (#021).
|
||||||
|
*
|
||||||
|
* Nur fuer die Dateien unter `prisma/seed-daten/vorlagen/` gedacht, damit der
|
||||||
|
* Seed seine Vorlagen am Schluessel erkennt und nicht am Namen, der sich
|
||||||
|
* aendern darf. Der **Import uebernimmt sie nicht** — eine eingespielte Kopie
|
||||||
|
* soll nicht den Platz des Originals einnehmen —, und der Export schreibt sie
|
||||||
|
* nicht, weil ein Feld, das beim Einlesen verfaellt, nur verwirrt.
|
||||||
|
*/
|
||||||
|
seedSchluessel: leerAlsUndefiniert(
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.max(60)
|
||||||
|
.regex(
|
||||||
|
/^[a-z0-9-]+$/,
|
||||||
|
"Der Seed-Schlüssel darf nur Kleinbuchstaben, Ziffern und Bindestriche enthalten.",
|
||||||
|
),
|
||||||
|
),
|
||||||
beschreibung: leerAlsUndefiniert(z.string().trim().max(GRENZEN.text)),
|
beschreibung: leerAlsUndefiniert(z.string().trim().max(GRENZEN.text)),
|
||||||
kategorie: leerAlsUndefiniert(z.string().trim().max(100)),
|
kategorie: leerAlsUndefiniert(z.string().trim().max(100)),
|
||||||
schlagworte: z.array(z.string().trim().min(1).max(100)).max(50).default([]),
|
schlagworte: z.array(z.string().trim().min(1).max(100)).max(50).default([]),
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ describe.runIf(datenbankVerfuegbar)("Handgeschriebene Datenbankobjekte", () => {
|
|||||||
await namen(
|
await namen(
|
||||||
`SELECT indexname AS name FROM pg_indexes WHERE schemaname='public' AND indexdef LIKE '%WHERE%'`,
|
`SELECT indexname AS name FROM pg_indexes WHERE schemaname='public' AND indexdef LIKE '%WHERE%'`,
|
||||||
),
|
),
|
||||||
).toEqual(["Kunde_eigene_organisation_einmalig", "VorlagenVersion_ein_entwurf"]);
|
).toEqual([
|
||||||
|
"Kunde_eigene_organisation_einmalig",
|
||||||
|
"Vorlage_seedSchluessel_einmalig",
|
||||||
|
"VorlagenVersion_ein_entwurf",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hat alle Prüfbedingungen", async () => {
|
it("hat alle Prüfbedingungen", async () => {
|
||||||
|
|||||||
@@ -321,6 +321,27 @@ describe.runIf(datenbankVerfuegbar)("Datenmodell: Regeln in der Datenbank", () =
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Vorlage", () => {
|
||||||
|
it("haelt den Seed-Schluessel eindeutig, wo er gesetzt ist", async () => {
|
||||||
|
await testPrisma.vorlage.create({
|
||||||
|
data: { name: "PC Neuinstallation", seedSchluessel: "pc-neuinstallation" },
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
testPrisma.vorlage.create({
|
||||||
|
data: { name: "Anders benannt", seedSchluessel: "pc-neuinstallation" },
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/Vorlage_seedSchluessel_einmalig/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("laesst beliebig viele Vorlagen ohne Seed-Schluessel zu", async () => {
|
||||||
|
// Der Index ist partiell, sonst waere die zweite eigene Vorlage eine
|
||||||
|
// Dublette von null (#021).
|
||||||
|
await testPrisma.vorlage.create({ data: { name: "Eigene A" } });
|
||||||
|
await testPrisma.vorlage.create({ data: { name: "Eigene B" } });
|
||||||
|
expect(await testPrisma.vorlage.count({ where: { seedSchluessel: null } })).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Vorlagen-Version", () => {
|
describe("Vorlagen-Version", () => {
|
||||||
it("haelt die Versionsnummer je Vorlage eindeutig", async () => {
|
it("haelt die Versionsnummer je Vorlage eindeutig", async () => {
|
||||||
const vorlage = await testPrisma.vorlage.create({ data: { name: "PC Neuinstallation" } });
|
const vorlage = await testPrisma.vorlage.create({ data: { name: "PC Neuinstallation" } });
|
||||||
|
|||||||
@@ -342,6 +342,59 @@ describe.runIf(datenbankVerfuegbar)("Seed gegen die Datenbank", () => {
|
|||||||
expect(await testPrisma.vorlagenVersion.count({ where: { vorlageId: erste.vorlageId } })).toBe(2);
|
expect(await testPrisma.vorlagenVersion.count({ where: { vorlageId: erste.vorlageId } })).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("erkennt eine umbenannte Vorlage am Schluessel und legt keine zweite an", async () => {
|
||||||
|
// Der Fall, fuer den das Feld da ist (#021). Vorher erkannte der Seed seine
|
||||||
|
// Vorlagen am Namen: wer eine umbenannte, bekam beim naechsten Lauf eine
|
||||||
|
// zweite - und der Schnellstart stand auf der Geraeteseite doppelt.
|
||||||
|
const [erste] = await vorlagenSaeen(testPrisma);
|
||||||
|
if (!erste) throw new Error("Der Seed hat keine Vorlage angelegt.");
|
||||||
|
await testPrisma.vorlage.update({
|
||||||
|
where: { id: erste.vorlageId },
|
||||||
|
data: { name: "Von Hand umbenannt" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const zweit = await vorlagenSaeen(testPrisma);
|
||||||
|
expect(zweit.every((eintrag) => !eintrag.neu)).toBe(true);
|
||||||
|
expect(await testPrisma.vorlage.count()).toBe(7);
|
||||||
|
// Und der Name bleibt der, den jemand gewaehlt hat.
|
||||||
|
const nachher = await testPrisma.vorlage.findUniqueOrThrow({ where: { id: erste.vorlageId } });
|
||||||
|
expect(nachher.name).toBe("Von Hand umbenannt");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("legt zu einer umbenannten Vorlage keine zweite Schnellstart-Zeile an", async () => {
|
||||||
|
const gesaet = await vorlagenSaeen(testPrisma);
|
||||||
|
await typVorgabenSaeen(testPrisma, gesaet);
|
||||||
|
const vorher = await testPrisma.geraetetypVorlage.count();
|
||||||
|
|
||||||
|
// „PC Neuinstallation“ steht im Schnellstart fuer PC und NOTEBOOK - genau die
|
||||||
|
// Zeilen, die sich vorher verdoppelt haben.
|
||||||
|
const pc = gesaet.find((eintrag) => eintrag.name === "PC Neuinstallation");
|
||||||
|
if (!pc) throw new Error("Die PC-Vorlage fehlt im Seed.");
|
||||||
|
await testPrisma.vorlage.update({
|
||||||
|
where: { id: pc.vorlageId },
|
||||||
|
data: { name: "Arbeitsplatz neu aufsetzen" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const nochmal = await vorlagenSaeen(testPrisma);
|
||||||
|
expect(await typVorgabenSaeen(testPrisma, nochmal)).toBe(0);
|
||||||
|
expect(await testPrisma.geraetetypVorlage.count()).toBe(vorher);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("traegt den Schluessel bei einer Installation nach, die ihn noch nicht hat", async () => {
|
||||||
|
// Eine Installation von vor diesem Feld: die Zeilen stehen, der Schluessel
|
||||||
|
// fehlt. Ohne den Rueckfall auf den Namen bekaeme sie beim ersten Lauf
|
||||||
|
// danach genau die Dubletten, die das Feld verhindern soll.
|
||||||
|
const gesaet = await vorlagenSaeen(testPrisma);
|
||||||
|
await testPrisma.vorlage.updateMany({ data: { seedSchluessel: null } });
|
||||||
|
expect(await testPrisma.vorlage.count({ where: { seedSchluessel: null } })).toBe(7);
|
||||||
|
|
||||||
|
const zweit = await vorlagenSaeen(testPrisma);
|
||||||
|
expect(zweit.every((eintrag) => !eintrag.neu)).toBe(true);
|
||||||
|
expect(await testPrisma.vorlage.count()).toBe(7);
|
||||||
|
expect(await testPrisma.vorlage.count({ where: { seedSchluessel: null } })).toBe(0);
|
||||||
|
expect(zweit.map((e) => e.vorlageId).sort()).toEqual(gesaet.map((e) => e.vorlageId).sort());
|
||||||
|
});
|
||||||
|
|
||||||
it("verknuepft die Schnellstart-Vorgaben mit den richtigen Vorlagen", async () => {
|
it("verknuepft die Schnellstart-Vorgaben mit den richtigen Vorlagen", async () => {
|
||||||
const gesaet = await vorlagenSaeen(testPrisma);
|
const gesaet = await vorlagenSaeen(testPrisma);
|
||||||
const angelegt = await typVorgabenSaeen(testPrisma, gesaet);
|
const angelegt = await typVorgabenSaeen(testPrisma, gesaet);
|
||||||
|
|||||||
@@ -354,6 +354,24 @@ describe.runIf(datenbankVerfuegbar)("Vorlagen-Austausch", () => {
|
|||||||
expect(await testPrisma.vorlagenVersion.count({ where: { vorlageId } })).toBe(1);
|
expect(await testPrisma.vorlagenVersion.count({ where: { vorlageId } })).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("gibt einer eingespielten Kopie keinen Seed-Schluessel", async () => {
|
||||||
|
// Sonst hielte der Seed die Kopie fuer das Original und legte das Original
|
||||||
|
// ein zweites Mal an (#021). Geprueft wird mit einem Dokument, das den
|
||||||
|
// Schluessel ausdruecklich mitbringt - ein Angreifer oder ein Versehen
|
||||||
|
// koennte ihn ja hineinschreiben.
|
||||||
|
const redakteur = await redakteurAnlegen();
|
||||||
|
const quelle = await beispielVersionAnlegen();
|
||||||
|
const austausch = await vorlageExportieren(quelle);
|
||||||
|
|
||||||
|
const { vorlageId } = await vorlageImportieren(
|
||||||
|
{ ...austausch, seedSchluessel: "pc-neuinstallation" },
|
||||||
|
redakteur.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const kopie = await testPrisma.vorlage.findUniqueOrThrow({ where: { id: vorlageId } });
|
||||||
|
expect(kopie.seedSchluessel).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("leitet die Positionen aus der Reihenfolge der Listen ab", async () => {
|
it("leitet die Positionen aus der Reihenfolge der Listen ab", async () => {
|
||||||
const redakteur = await redakteurAnlegen();
|
const redakteur = await redakteurAnlegen();
|
||||||
const { versionId } = await vorlageImportieren(
|
const { versionId } = await vorlageImportieren(
|
||||||
@@ -437,6 +455,19 @@ describe.runIf(datenbankVerfuegbar)("Vorlagen-Austausch", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Seed-Schluessel", () => {
|
||||||
|
it("steht in keinem Export – auch nicht bei einer mitgelieferten Vorlage", async () => {
|
||||||
|
// Ein Feld, das beim Einlesen verfaellt, gehoert nicht in die Datei: wer es
|
||||||
|
// dort sieht, haelt es fuer uebertragbar.
|
||||||
|
const quelle = await beispielVersionAnlegen();
|
||||||
|
await testPrisma.vorlage.updateMany({ data: { seedSchluessel: "pc-neuinstallation" } });
|
||||||
|
|
||||||
|
const austausch = await vorlageExportieren(quelle);
|
||||||
|
expect(austausch).not.toHaveProperty("seedSchluessel");
|
||||||
|
expect(JSON.stringify(austausch)).not.toContain("pc-neuinstallation");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Rundlauf", () => {
|
describe("Rundlauf", () => {
|
||||||
it("ergibt ein inhaltsgleiches Dokument – bis auf die Version", async () => {
|
it("ergibt ein inhaltsgleiches Dokument – bis auf die Version", async () => {
|
||||||
const redakteur = await redakteurAnlegen();
|
const redakteur = await redakteurAnlegen();
|
||||||
|
|||||||
Reference in New Issue
Block a user