feat(datenmodell): Schema, tagCodes und Geheimnisse (#20)
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>
This commit was merged in pull request #20.
This commit is contained in:
@@ -80,6 +80,37 @@ Schritte**. Damit der Schritt dorthin keine Migration braucht:
|
||||
- kein Codepfad nimmt an, dass ein Schritt nur von einem Menschen erledigt wird
|
||||
- das Krypto-Modul für das LDAP-Bind-Passwort ist schon der Platz für Connector-Geheimnisse
|
||||
|
||||
## 5a. Was in der Datenbank steht und warum
|
||||
|
||||
Die Service-Schicht ist der richtige Ort für Regeln mit verständlichen Meldungen. Sie ist aber
|
||||
nicht der einzige Weg, auf dem Daten entstehen: Migrationen, `psql`, Skripte und ab Phase 4 die
|
||||
Connectoren schreiben an ihr vorbei. Was stillschweigend falsch werden könnte, steht deshalb
|
||||
zusätzlich in der Datenbank:
|
||||
|
||||
| Regel | Form |
|
||||
|---|---|
|
||||
| Höchstens eine eigene Organisation | partieller Eindeutigkeitsindex |
|
||||
| Höchstens ein Entwurf je Vorlage | partieller Eindeutigkeitsindex |
|
||||
| Kundenkürzel, `tagCode`-Form, verschlüsseltes Bind-Passwort | CHECK auf die Form |
|
||||
| Registriertes Gerät braucht Hostname oder Bezeichnung | CHECK, greift erst ab `registriertAm` |
|
||||
| Mitarbeiter aus dem Verzeichnis **oder** Freitextname | CHECK |
|
||||
| Status und Zeitstempel laufen nicht auseinander | vier CHECKs |
|
||||
| `NICHT_ZUTREFFEND` und `FEHLGESCHLAGEN` brauchen eine Notiz | CHECK |
|
||||
| Einstellung ist eine einzelne Zeile | CHECK auf die feste Kennung |
|
||||
| **Mandantengrenze** | vier zusammengesetzte Fremdschlüssel |
|
||||
|
||||
Die Mandantengrenze verdient die Erklärung: `Lauf` verweist mit `("geraetId", "kundeId")` auf
|
||||
`Geraet("id", "kundeId")`, analog für Person und Abteilung, und `Geraet` ebenso auf den Besitzer.
|
||||
Damit ist ein Lauf, der ein Gerät eines fremden Kunden zieht, nicht mehr bloß ein Fehler im
|
||||
Service, sondern strukturell unmöglich. `MATCH SIMPLE` sorgt dafür, dass ein Lauf ohne Gerät
|
||||
weiterhin erlaubt ist.
|
||||
|
||||
Zwei Dinge sind bewusst **nicht** kaskadierend: ein `Lauf` nimmt seine Schrittergebnisse und
|
||||
Kommentare nicht mit, und ein `Protokoll`-Eintrag verliert seinen Urheber nicht. Beides stünde
|
||||
gegen den Zweck eines Archivs. Der `tagCode` ist zusätzlich in `TagCodeVergabe` festgehalten,
|
||||
aus dem nie gelöscht wird — sonst würde ein Code nach dem Löschen eines Geräts wieder frei und
|
||||
ein alter Aufkleber zeigte auf ein fremdes Objekt.
|
||||
|
||||
## 6. Betrieb: zwei Wege, eine Regel
|
||||
|
||||
Die Datenbank ist auf beiden Wegen von außen nicht erreichbar:
|
||||
|
||||
+63
-2
@@ -8,11 +8,12 @@ als Unterpunkte mit Aufgabe und Dauer.
|
||||
| Issue | Titel | Tatsächlich |
|
||||
|---|---|---|
|
||||
| #005 | M0 – Gerüst, Prozess, Design-Token | 1 h 25 min |
|
||||
| | **Summe** | **1 h 25 min** |
|
||||
| #006 | M1 – Datenmodell | 1 h 10 min |
|
||||
| | **Summe** | **2 h 35 min** |
|
||||
|
||||
| Meilenstein | Tatsächlich | Status |
|
||||
|---|---|---|
|
||||
| v0.1.0 | 1 h 25 min | in Arbeit |
|
||||
| v0.1.0 | 2 h 35 min | in Arbeit |
|
||||
|
||||
---
|
||||
|
||||
@@ -104,3 +105,63 @@ als Unterpunkte mit Aufgabe und Dauer.
|
||||
|
||||
- #006 Datenmodell: das vollständige Schema in deutscher Benennung, `tagCode`-Generator,
|
||||
Krypto-Helfer, erste Migration, idempotenter Seed-Rumpf.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-25 · Sitzung 2 · 00:05–01:15
|
||||
|
||||
**Issue:** #006 (M1)
|
||||
**Version:** 0.1.0
|
||||
|
||||
### Erledigt
|
||||
|
||||
- Vollständiges Schema in deutscher Benennung: 20 Tabellen, 9 Aufzählungen, eine Migration
|
||||
- `src/lib/tagcode.ts` und `src/lib/geheimnisse.ts` samt Tests, parallel von einem Sub-Agenten
|
||||
gebaut, während ich das Schema schrieb (Dauer dort: rund 10 min)
|
||||
- Idempotenter Seed für Betriebsmodus, eigene Organisation und Administrator — zweimal
|
||||
hintereinander gefahren, keine Zeile geändert
|
||||
- Unabhängiger Review-Agent gegen die Akzeptanzkriterien; alle Blocker und alle neun „sollte
|
||||
behoben werden"-Punkte abgearbeitet
|
||||
- **106 Tests** grün (vorher 11), davon 42 gegen die echten Datenbankregeln
|
||||
|
||||
### Entscheidungen
|
||||
|
||||
- **Regeln gehören zusätzlich in die Datenbank.** Zwölf CHECK-Bedingungen, zwei partielle
|
||||
Eindeutigkeitsindizes und vier zusammengesetzte Fremdschlüssel. Begründung: die Service-Schicht
|
||||
ist nicht der einzige Weg, auf dem Daten entstehen — Migrationen, `psql`, Skripte und ab
|
||||
Phase 4 die Connectoren schreiben an ihr vorbei.
|
||||
- **Die Mandantengrenze wird strukturell erzwungen.** `Lauf` verweist mit
|
||||
`("geraetId", "kundeId")` auf `Geraet("id", "kundeId")`, analog für Person und Abteilung.
|
||||
`docs/architektur.md` nennt die Grenze eine Sicherheitsgrenze — dann soll sie nicht allein an
|
||||
`kundeErmitteln()` hängen. `MATCH SIMPLE` lässt einen Lauf ohne Gerät weiter zu.
|
||||
- **Kein Cascade dort, wo das Archiv hängt.** Ein `DELETE` auf einen Lauf hätte Schrittergebnisse
|
||||
und Kommentare stillschweigend mitgenommen, ein Protokolleintrag hätte seinen Urheber verlieren
|
||||
können. Beides jetzt `Restrict`.
|
||||
- **`TagCodeVergabe` als Vergabebuch.** Die Eindeutigkeit allein aus `Geraet.tagCode` zu ziehen
|
||||
reicht nicht: verschwindet die Zeile, wäre der Code wieder frei und ein alter Aufkleber im
|
||||
Regal zeigte auf ein fremdes Objekt.
|
||||
- **Widerspruch im Auftrag beim tagCode-Alphabet.** Das wörtliche Alphabet enthält ein `L`,
|
||||
die Klammer daneben verbietet es. Aufgelöst zugunsten des wörtlichen Alphabets: die Klammer
|
||||
benennt Verwechslungsgruppen, keine Ausschlussliste — aus `{1, I, L}` sind `1` und `I` weg,
|
||||
dadurch ist `L` eindeutig. Das Akzeptanzkriterium in #006 ist richtiggestellt worden, statt
|
||||
einen grünen Test darüber zu setzen.
|
||||
- **Ein Systemlauf muss darstellbar sein.** `Lauf.erstelltVonId` ist optional, wie schon
|
||||
`Schrittergebnis.aktualisiertVonId`. Wiederkehrende Läufe und der Start per Webhook (Phase 4)
|
||||
haben keinen Menschen dahinter.
|
||||
|
||||
### Probleme
|
||||
|
||||
- **`prisma migrate reset --force` zeigte den Bestätigungsdialog trotzdem** und ließ einen
|
||||
Schema-Engine-Prozess zurück, der die Advisory-Sperre hielt; der nächste `migrate dev` lief
|
||||
daraufhin in einen Timeout. Verlässlicher: `DROP SCHEMA public CASCADE` per psql, dann
|
||||
`migrate dev --create-only < /dev/null`.
|
||||
- **Port 5433 ist von ProxLedger belegt.** Die Entwicklungsdatenbank liegt jetzt auf 5434,
|
||||
umstellbar über `DEV_DB_PORT`.
|
||||
- **Eine Regex-Ersetzung hat `quelle.length` auch aus der Längenprüfung gerissen** und den
|
||||
Krypto-Helfer unübersetzbar gemacht. Die Testsuite hat es sofort gezeigt — ein Argument mehr
|
||||
dafür, nach jeder Sammeländerung wirklich alle Schranken laufen zu lassen.
|
||||
|
||||
### Nächster Schritt
|
||||
|
||||
- #007 Auth, Rollen, Benutzer: `openid-client` mit Discovery, `jose`-Sitzungscookie, lokaler
|
||||
Login mit argon2, `RECHTE`-Tabelle, Login-Seite mit Rücksprung auf die ursprüngliche URL.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# #006 M1 – Datenmodell
|
||||
|
||||
**Status:** offen
|
||||
**Status:** erledigt
|
||||
**Meilenstein:** v0.1.0
|
||||
**Labels:** phase-1, backend
|
||||
**Geschätzt:** 5 h **Tatsächlich:** –
|
||||
**Geschätzt:** 5 h **Tatsächlich:** 1 h 10 min
|
||||
|
||||
## Ziel
|
||||
|
||||
@@ -11,23 +11,49 @@ Das vollständige Schema in deutscher Benennung, sodass alle folgenden Meilenste
|
||||
|
||||
## Aufgaben
|
||||
|
||||
- [ ] Prisma 7: Generator `prisma-client` mit `output = "../src/generated/prisma"`, `datasource` ohne `url`, `@prisma/adapter-pg`
|
||||
- [ ] `prisma.config.ts` mit `migrations.seed = "tsx prisma/seed.ts"`
|
||||
- [ ] `src/lib/db.ts`: Lazy-Singleton-Proxy, damit `next build` ohne `DATABASE_URL` läuft, Dev-Instanz an `globalThis`
|
||||
- [ ] Modelle: `Benutzer`, `Kunde`, `Einstellung`, `Verzeichnisquelle`, `Mitarbeiter`, `Abteilung`, `Geraet`, `GeraetetypVorlage`, `Vorlage`, `VorlagenVersion`, `VorlagenVariable`, `VorlagenAbschnitt`, `VorlagenSchritt`, `Lauf`, `Schrittergebnis`, `Kommentar`, `Protokoll`, `AbgleichSperre`
|
||||
- [ ] Enums: `Rolle`, `Betriebsmodus`, `LaufStatus`, `SchrittZustand`, `SchrittTyp`, `GeraeteTyp`, `VersionStatus`, `Herkunft`
|
||||
- [ ] Unique-Constraints: `Geraet.tagCode`, `(laufId, vorlagenSchrittId)` auf `Schrittergebnis`, `(quelleId, externeId)` auf `Mitarbeiter`, `(kundeId, name)` auf `Abteilung`, `(kundeId)` partiell für `isteigeneOrganisation`
|
||||
- [ ] `pg_trgm`-Erweiterung und GIN-Indizes für die Archivsuche über `Lauf.titel`, `Schrittergebnis.notiz`, `Kommentar.rumpf`
|
||||
- [ ] Jede kundenbezogene Zeile trägt `kundeId` als Pflichtfeld: `Geraet`, `Mitarbeiter`, `Abteilung`, `Verzeichnisquelle`, `Lauf`
|
||||
- [ ] `src/lib/tagcode.ts`: Alphabet `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`, Präfix `LZ-`, 6 Zeichen, Kollisionsprüfung serverseitig
|
||||
- [ ] `src/lib/geheimnisse.ts`: AES-256-GCM aus `APP_SECRET` (scrypt-abgeleitet), für `bindPasswortVerschluesselt`
|
||||
- [ ] Erste Migration, `prisma/seed.ts` als idempotenter Rumpf: Admin aus `SEED_ADMIN_EMAIL`/`SEED_ADMIN_PASSWORD` mit `@node-rs/argon2`, eigene Organisation, `Einstellung` mit `Betriebsmodus.INTERN`
|
||||
- [ ] `tests/setup-env.ts`, `tests/global-setup.ts`, `tests/helpers/db.ts` nach ProxLedger-Muster
|
||||
- [x] Prisma 7: Generator `prisma-client` mit `output = "../src/generated/prisma"`, `datasource` ohne `url`, `@prisma/adapter-pg`
|
||||
- [x] `prisma.config.ts` mit `migrations.seed = "tsx prisma/seed.ts"`
|
||||
- [x] `src/lib/db.ts`: Lazy-Singleton-Proxy, damit `next build` ohne `DATABASE_URL` läuft, Dev-Instanz an `globalThis`
|
||||
- [x] Modelle: `Benutzer`, `Kunde`, `Einstellung`, `Verzeichnisquelle`, `Mitarbeiter`, `Abteilung`, `Geraet`, `GeraetetypVorlage`, `Vorlage`, `VorlagenVersion`, `VorlagenVariable`, `VorlagenAbschnitt`, `VorlagenSchritt`, `Lauf`, `Schrittergebnis`, `Kommentar`, `Protokoll`, `AbgleichSperre`
|
||||
- [x] Enums: `Rolle`, `Betriebsmodus`, `LaufStatus`, `SchrittZustand`, `SchrittTyp`, `GeraeteTyp`, `VersionStatus`, `Herkunft`
|
||||
- [x] Unique-Constraints: `Geraet.tagCode`, `(laufId, vorlagenSchrittId)` auf `Schrittergebnis`, `(quelleId, externeId)` auf `Mitarbeiter`, `(kundeId, name)` auf `Abteilung`, `(kundeId)` partiell für `isteigeneOrganisation`
|
||||
- [x] `pg_trgm`-Erweiterung und GIN-Indizes für die Archivsuche über `Lauf.titel`, `Schrittergebnis.notiz`, `Kommentar.rumpf`
|
||||
- [x] Jede kundenbezogene Zeile trägt `kundeId` als Pflichtfeld: `Geraet`, `Mitarbeiter`, `Abteilung`, `Verzeichnisquelle`, `Lauf`
|
||||
- [x] `src/lib/tagcode.ts`: Alphabet `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`, Präfix `LZ-`, 6 Zeichen, Kollisionsprüfung serverseitig
|
||||
- [x] `src/lib/geheimnisse.ts`: AES-256-GCM aus `APP_SECRET` (scrypt-abgeleitet), für `bindPasswortVerschluesselt`
|
||||
- [x] Erste Migration, `prisma/seed.ts` als idempotenter Rumpf: Admin aus `SEED_ADMIN_EMAIL`/`SEED_ADMIN_PASSWORD` mit `@node-rs/argon2`, eigene Organisation, `Einstellung` mit `Betriebsmodus.INTERN`
|
||||
- [x] `tests/setup-env.ts`, `tests/global-setup.ts`, `tests/helpers/db.ts` nach ProxLedger-Muster
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- [ ] `prisma migrate deploy` läuft auf leerer Datenbank durch
|
||||
- [ ] `pnpm db:seed` zweimal hintereinander ohne Fehler und ohne Datenänderung
|
||||
- [ ] Tests: `tagCode` benutzt nur das erlaubte Alphabet, enthält nie `0`, `O`, `1`, `I` oder `L`, ist eindeutig über 10 000 Ziehungen
|
||||
- [ ] Tests: verschlüsseln und entschlüsseln ist verlustfrei, zwei Verschlüsselungen desselben Klartexts unterscheiden sich (eigener IV), ein manipulierter Geheimtext wirft
|
||||
- [ ] `next build` gelingt ohne gesetzte `DATABASE_URL`
|
||||
- [x] `prisma migrate deploy` läuft auf leerer Datenbank durch
|
||||
- [x] `pnpm db:seed` zweimal hintereinander ohne Fehler und ohne Datenänderung
|
||||
- [x] Tests: `tagCode` benutzt nur das vorgegebene Alphabet `ABCDEFGHJKLMNPQRSTUVWXYZ23456789` und enthält damit nie `0`, `O`, `1` oder `I`; eindeutig über 10 000 Ziehungen
|
||||
- [x] Tests: verschlüsseln und entschlüsseln ist verlustfrei, zwei Verschlüsselungen desselben Klartexts unterscheiden sich (eigener IV), ein manipulierter Geheimtext wirft
|
||||
- [x] `next build` gelingt ohne gesetzte `DATABASE_URL`
|
||||
|
||||
## Beim Bauen aufgefallen
|
||||
|
||||
- **Widerspruch im Auftrag beim tagCode-Alphabet, aufgelöst.** Das wörtlich vorgegebene Alphabet
|
||||
`ABCDEFGHJKLMNPQRSTUVWXYZ23456789` enthält ein `L`, obwohl die Klammer daneben „ohne 0/O,
|
||||
1/I/L" sagt. Beides zugleich geht nicht: ohne `L` blieben 31 Zeichen. Wir folgen dem wörtlichen
|
||||
Alphabet, weil die Klammer die *Verwechslungsgruppen* benennt und keine Ausschlussliste ist:
|
||||
aus `{0, O}` sind beide weg, aus `{1, I, L}` sind `1` und `I` weg — dadurch ist `L` eindeutig.
|
||||
Das Präfix `LZ-` enthält ohnehin in jedem Code ein `L`. Das Akzeptanzkriterium oben ist
|
||||
entsprechend richtiggestellt; ein grüner Test über einem unerfüllten Kriterium wäre schlimmer
|
||||
als ein korrigiertes Kriterium.
|
||||
- **Der unabhängige Review hat drei Lücken gefunden, die alle behoben sind:** der geforderten
|
||||
Suchspalte `Schrittergebnis.wert` fehlte der Trigramm-Index; die im Lauf gewählte Abteilung war
|
||||
nur ein Wert in `variablen` statt ein Bezug, wodurch die Druckansicht eines archivierten Laufs
|
||||
nicht reproduzierbar gewesen wäre; und für die Kundenbindung eines Benutzers (#008) gab es
|
||||
keine Struktur, sie hätte also nur im Token stehen können — gegen die eigene Regel, Rechte pro
|
||||
Anfrage aus der Datenbank zu lesen.
|
||||
- **Ein `DELETE` auf einen Lauf hätte Schrittergebnisse und Kommentare stillschweigend
|
||||
mitgenommen.** Jetzt `Restrict`; ein Lauf lässt sich nur noch bewusst und in der richtigen
|
||||
Reihenfolge entfernen. Dasselbe für das `Protokoll`, das seinen Urheber nicht mehr verlieren
|
||||
kann.
|
||||
- **`tagCode` wäre nach dem Löschen eines Geräts wieder frei gewesen.** Deshalb `TagCodeVergabe`
|
||||
als Vergabebuch, aus dem nie gelöscht wird.
|
||||
- **`prisma migrate reset --force` hat den Bestätigungsdialog trotzdem gezeigt** und die
|
||||
Advisory-Sperre gehalten. Schneller und verlässlicher: `DROP SCHEMA public CASCADE` per psql,
|
||||
dann `migrate dev --create-only`.
|
||||
|
||||
@@ -10,7 +10,7 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
|
||||
| #003 | [Phase 3: Protokoll, bedingte Schritte, Freigabe](003-epic-phase-3.md) | offen | v0.3.0 | – | – |
|
||||
| #004 | [Phase 4: Aktionen, Connectors, REST-API](004-epic-phase-4.md) | offen | v0.4.0 | – | – |
|
||||
| #005 | [M0 – Gerüst, Prozess, Design-Token](005-geruest-prozess-design.md) | erledigt | v0.1.0 | 6 h | 1 h 25 min |
|
||||
| #006 | [M1 – Datenmodell](006-datenmodell.md) | offen | v0.1.0 | 5 h | – |
|
||||
| #006 | [M1 – Datenmodell](006-datenmodell.md) | erledigt | v0.1.0 | 5 h | 1 h 10 min |
|
||||
| #007 | [M2 – Auth, Rollen, Benutzer](007-auth-rollen-benutzer.md) | offen | v0.1.0 | 8 h | – |
|
||||
| #008 | [M3 – Betriebsmodus und Kunden](008-betriebsmodus-kunden.md) | offen | v0.1.0 | 6 h | – |
|
||||
| #009 | [M4 – Vorlagen-Versionierung im Service](009-vorlagen-versionierung.md) | offen | v0.1.0 | 6 h | – |
|
||||
@@ -24,4 +24,4 @@ Diese Dateien sind die Quelle; `pnpm issues:sync` spiegelt sie in den Gitea-Trac
|
||||
| #017 | [M12 – Seed der sieben Vorlagen](017-seed-sieben-vorlagen.md) | offen | v0.1.0 | 8 h | – |
|
||||
| #018 | [M13 – E2E-Tests, Anleitung, PDF](018-e2e-anleitung-pdf.md) | offen | v0.1.0 | 14 h | – |
|
||||
| #019 | [M14 – Betriebswege und Release v0.1.0](019-betrieb-release.md) | offen | v0.1.0 | 10 h | – |
|
||||
| | **Summe** | | | **136 h** | **1 h 25 min** |
|
||||
| | **Summe** | | | **136 h** | **2 h 35 min** |
|
||||
|
||||
Reference in New Issue
Block a user