feat(datenmodell): Schema, tagCodes und Geheimnisse
Das vollständige Datenmodell in deutscher Benennung: 20 Tabellen, 9 Aufzählungen, eine Migration, dazu die beiden framework-freien Helfer für die tagCodes der QR-Aufkleber und für verschlüsselte Geheimnisse. Die tragende Entscheidung ist, wo die Regeln stehen. Die Service-Schicht ist der richtige Ort für verständliche Meldungen, aber nicht der einzige Weg, auf dem Daten entstehen — Migrationen, psql, Skripte und ab Phase 4 die Connectoren schreiben an ihr vorbei. Was dabei stillschweigend falsch werden könnte, steht deshalb zusätzlich in der Datenbank: zwölf CHECK-Bedingungen, zwei partielle Eindeutigkeitsindizes und vier zusammengesetzte Fremdschlüssel. Die Mandantengrenze ist einer davon. Lauf verweist mit (geraetId, kundeId) auf Geraet(id, kundeId), analog für Person und Abteilung, und Geraet ebenso auf seinen Besitzer. Ein Lauf, der ein Gerät eines fremden Kunden zieht, ist damit nicht mehr bloß ein Fehler im Service, sondern strukturell unmöglich. MATCH SIMPLE lässt einen Lauf ohne Gerät weiterhin zu. Zwei Dinge kaskadieren bewusst nicht: ein Lauf nimmt seine Schrittergebnisse und Kommentare nicht mit, und ein Protokolleintrag verliert seinen Urheber nicht. Beides stünde gegen den Zweck eines Archivs. Aus demselben Grund gibt es TagCodeVergabe: ohne dieses Buch wäre ein tagCode nach dem Löschen eines Geräts wieder frei, und ein alter Aufkleber im Regal zeigte auf ein fremdes Objekt. Ein unabhängiger Review-Agent hat drei Lücken gefunden, die alle behoben sind: der im Auftrag 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. Das tagCode-Alphabet des Auftrags widerspricht sich: der wörtlich vorgegebene Zeichensatz enthält ein L, die Klammer daneben verbietet es. Wir folgen dem Wortlaut, weil die Klammer Verwechslungsgruppen benennt und keine Ausschlussliste ist — aus {1, I, L} sind 1 und I gestrichen, wodurch L eindeutig wird, und das Präfix LZ- enthält ohnehin in jedem Code ein L. Das Akzeptanzkriterium in #006 ist richtiggestellt worden statt umgekehrt. 106 Tests, davon 42 gegen die echten Datenbankregeln. Closes #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
49ce06f206
commit
0e79bebd33
+579
-9
@@ -1,7 +1,19 @@
|
||||
// Laufzettel – Datenmodell
|
||||
// Benennung deutsch, Umlaute ASCII-gefaltet (siehe CLAUDE.md, Abschnitt Namensabbildung).
|
||||
// Das vollstaendige Schema entsteht in #006; hier steht nur so viel, wie der
|
||||
// Generator zum Laufen braucht.
|
||||
//
|
||||
// Benennung deutsch, Umlaute ASCII-gefaltet. Die Abbildung auf die englischen
|
||||
// Modellnamen der Auftragsvorgabe steht in CLAUDE.md.
|
||||
//
|
||||
// Zwei Entwurfsentscheidungen tragen das Ganze, siehe docs/architektur.md:
|
||||
//
|
||||
// 1. Jede kundenbezogene Zeile traegt IMMER eine kundeId – auch im Modus
|
||||
// INTERN, wo es die eine Organisation mit istEigeneOrganisation = true ist.
|
||||
// Dadurch ist der Wechsel in den Modus DIENSTLEISTUNG eine Datenaenderung
|
||||
// und keine Migration.
|
||||
//
|
||||
// 2. Nichts wird geloescht, woran ein Lauf haengt. Geraete, Abteilungen und
|
||||
// Vorlagen werden archiviert, Mitarbeiter nur auf aktiv = false gesetzt.
|
||||
// Deshalb stehen die Fremdschluessel auf Restrict; Cascade gibt es nur
|
||||
// innerhalb einer Vorlagen-Version, die als Ganzes verworfen werden darf.
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
@@ -12,6 +24,8 @@ datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// ---- Aufzaehlungen ---------------------------------------------------------
|
||||
|
||||
enum Rolle {
|
||||
ADMIN
|
||||
REDAKTEUR
|
||||
@@ -19,16 +33,572 @@ enum Rolle {
|
||||
LESER
|
||||
}
|
||||
|
||||
enum Betriebsmodus {
|
||||
INTERN
|
||||
DIENSTLEISTUNG
|
||||
}
|
||||
|
||||
enum LaufStatus {
|
||||
OFFEN
|
||||
IN_ARBEIT
|
||||
ABGESCHLOSSEN
|
||||
ARCHIVIERT
|
||||
}
|
||||
|
||||
enum SchrittZustand {
|
||||
OFFEN
|
||||
ERLEDIGT
|
||||
NICHT_ZUTREFFEND
|
||||
FEHLGESCHLAGEN
|
||||
}
|
||||
|
||||
// AKTION kommt in Phase 4 dazu. Weil der Typ ein Enum ist und die Einzelheiten
|
||||
// in VorlagenSchritt.konfig liegen, braucht das keine Migration der bestehenden
|
||||
// Schritte.
|
||||
enum SchrittTyp {
|
||||
HAKEN
|
||||
TEXT
|
||||
ZAHL
|
||||
AUSWAHL
|
||||
DATUM
|
||||
BEFEHL
|
||||
}
|
||||
|
||||
enum VariablenTyp {
|
||||
TEXT
|
||||
ZAHL
|
||||
AUSWAHL
|
||||
DATUM
|
||||
}
|
||||
|
||||
enum GeraeteTyp {
|
||||
PC
|
||||
NOTEBOOK
|
||||
SERVER
|
||||
FIREWALL
|
||||
SWITCH
|
||||
DRUCKER
|
||||
RAUM
|
||||
SONSTIGES
|
||||
}
|
||||
|
||||
enum VersionStatus {
|
||||
ENTWURF
|
||||
VEROEFFENTLICHT
|
||||
}
|
||||
|
||||
enum Herkunft {
|
||||
ABGLEICH
|
||||
MANUELL
|
||||
}
|
||||
|
||||
// ---- Einstellungen und Mandanten ------------------------------------------
|
||||
|
||||
/// Einzelne Zeile mit fester id "global". Der Betriebsmodus liegt bewusst in
|
||||
/// der Datenbank und nicht in der Umgebung, damit der Admin ohne Neustart
|
||||
/// umschalten kann.
|
||||
model Einstellung {
|
||||
id String @id @default("global")
|
||||
modus Betriebsmodus @default(INTERN)
|
||||
/// Globale Namensregeln fuer upn, sam, mail und hostname. Im Modus
|
||||
/// DIENSTLEISTUNG koennen Kunden eigene Regeln haben, die diese ueberlagern.
|
||||
namensregeln Json @default("{}")
|
||||
/// Ziel-OU fuer deaktivierte Konten, Vorbelegung im Offboarding.
|
||||
gesperrteOu String?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Kunde {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
/// 2 bis 6 Zeichen, erscheint auf Etiketten und in der Hostname-Regel.
|
||||
kuerzel String @unique
|
||||
/// Genau eine Zeile traegt true. Erzwungen durch einen partiellen eindeutigen
|
||||
/// Index in der Migration, den Prisma selbst nicht ausdruecken kann.
|
||||
istEigeneOrganisation Boolean @default(false)
|
||||
/// Namensregeln dieses Kunden; fehlende Schluessel fallen auf die globalen
|
||||
/// aus Einstellung zurueck.
|
||||
namensregeln Json @default("{}")
|
||||
notizen String?
|
||||
archiviert Boolean @default(false)
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
abteilungen Abteilung[]
|
||||
benutzerbindungen BenutzerKunde[]
|
||||
geraete Geraet[]
|
||||
laeufe Lauf[]
|
||||
mitarbeiter Mitarbeiter[]
|
||||
verzeichnisquellen Verzeichnisquelle[]
|
||||
vorlagen Vorlage[]
|
||||
|
||||
@@index([archiviert])
|
||||
}
|
||||
|
||||
// ---- Benutzer der Anwendung ------------------------------------------------
|
||||
|
||||
/// Die Menschen, die Laufzettel bedienen – nicht zu verwechseln mit Mitarbeiter,
|
||||
/// den Personen der Firma aus dem Verzeichnis.
|
||||
model Benutzer {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String
|
||||
rolle Rolle @default(LESER)
|
||||
/// Subject-Claim des OIDC-Anbieters. Leer bei rein lokalen Konten.
|
||||
oidcSub String? @unique
|
||||
/// Nur gesetzt, wenn der lokale Login benutzt wird. argon2, nie Klartext.
|
||||
passwortHash String?
|
||||
aktiv Boolean @default(true)
|
||||
/// Sitzungen, die vor diesem Zeitpunkt ausgestellt wurden, gelten nicht mehr.
|
||||
/// So laesst sich ein Konto sperren, ohne auf den Ablauf des Cookies zu warten.
|
||||
sitzungenUngueltigAb DateTime?
|
||||
letzteAnmeldungAm DateTime?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
abgeschlosseneLaeufe Lauf[] @relation("LaufAbgeschlossenVon")
|
||||
archivierteLaeufe Lauf[] @relation("LaufArchiviertVon")
|
||||
erstellteLaeufe Lauf[] @relation("LaufErstelltVon")
|
||||
zugewieseneLaeufe Lauf[] @relation("LaufZugewiesenAn")
|
||||
kommentare Kommentar[]
|
||||
kundenbindungen BenutzerKunde[]
|
||||
protokolle Protokoll[]
|
||||
schrittergebnisse Schrittergebnis[]
|
||||
veroeffentlichteVersionen VorlagenVersion[] @relation("VersionVeroeffentlichtVon")
|
||||
|
||||
@@index([aktiv])
|
||||
@@index([rolle])
|
||||
}
|
||||
|
||||
/// Bindet einen Benutzer an Kunden. Nur im Modus DIENSTLEISTUNG und nur, wenn
|
||||
/// OIDC_CUSTOMER_GROUP_PREFIX gesetzt ist: TECHNIKER und LESER sehen dann allein
|
||||
/// ihre Kunden, ADMIN und REDAKTEUR alles.
|
||||
///
|
||||
/// Die Bindung liegt in der Datenbank und nicht im Token, damit sie – wie Rolle
|
||||
/// und Aktiv-Status – pro Anfrage frisch gelesen wird und auch fuer lokale Konten
|
||||
/// gilt. Beim OIDC-Login wird sie neu geschrieben.
|
||||
model BenutzerKunde {
|
||||
benutzerId String
|
||||
kundeId String
|
||||
erstelltAm DateTime @default(now())
|
||||
|
||||
benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Cascade)
|
||||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([benutzerId, kundeId])
|
||||
@@index([kundeId])
|
||||
}
|
||||
|
||||
// ---- Verzeichnis und Personen ---------------------------------------------
|
||||
|
||||
/// LDAP-Quelle fuer den Abgleich des Mitarbeiterverzeichnisses. In der Regel
|
||||
/// genau eine (das Firmen-AD); mehrere sind erlaubt, etwa eine zweite Domaene.
|
||||
/// Nur fuer die eigene Organisation – im Modus DIENSTLEISTUNG gibt es keinen
|
||||
/// Abgleich zu Kunden-ADs.
|
||||
model Verzeichnisquelle {
|
||||
id String @id @default(cuid())
|
||||
kundeId String
|
||||
name String
|
||||
aktiv Boolean @default(false)
|
||||
url String
|
||||
bindDn String
|
||||
/// AES-256-GCM aus APP_SECRET. Darf keine API-Antwort erreichen; dafuer sorgt
|
||||
/// der Serializer in der Service-Schicht, abgedeckt durch einen eigenen Test.
|
||||
bindPasswortVerschluesselt String
|
||||
basisDn String
|
||||
filter String
|
||||
/// Zuordnung der LDAP-Attribute auf unsere Felder, mit AD-Vorgaben.
|
||||
attributZuordnung Json
|
||||
/// Eigenes CA-Zertifikat, falls ldaps gegen eine interne CA laeuft.
|
||||
caZertifikatPem String?
|
||||
abgleichIntervallMinuten Int @default(60)
|
||||
letzterAbgleichAm DateTime?
|
||||
letzterAbgleichStatus String?
|
||||
/// Nimmt die Meldung der LDAP-Bibliothek auf. #016 muss sie filtern, bevor sie
|
||||
/// hier landet – rohe Bind-Fehler koennen das Passwort enthalten.
|
||||
letzterAbgleichFehler String?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||||
mitarbeiter Mitarbeiter[]
|
||||
|
||||
@@index([kundeId])
|
||||
@@index([aktiv])
|
||||
}
|
||||
|
||||
/// Die Personen der Firma bzw. des Kunden. Werden nie geloescht, nur auf
|
||||
/// aktiv = false gesetzt, damit archivierte Laeufe weiter auf sie zeigen.
|
||||
model Mitarbeiter {
|
||||
id String @id @default(cuid())
|
||||
kundeId String
|
||||
/// Nur bei Herkunft ABGLEICH gesetzt.
|
||||
quelleId String?
|
||||
/// objectGUID des Verzeichnisses, je Quelle eindeutig.
|
||||
externeId String?
|
||||
vorname String
|
||||
nachname String
|
||||
anzeigename String
|
||||
mail String?
|
||||
upn String?
|
||||
sam String?
|
||||
dn String?
|
||||
/// Der rohe department-Wert aus dem Verzeichnis. Die gepflegten Abteilungen
|
||||
/// stehen in Abteilung und werden ueber den Namen zugeordnet; ein Wert ohne
|
||||
/// passende Abteilung bleibt trotzdem erhalten.
|
||||
abteilung String?
|
||||
titel String?
|
||||
vorgesetzterExterneId String?
|
||||
aktiv Boolean @default(true)
|
||||
herkunft Herkunft @default(MANUELL)
|
||||
zuletztGesehenAm DateTime?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||||
quelle Verzeichnisquelle? @relation(fields: [quelleId], references: [id], onDelete: Restrict)
|
||||
geraete Geraet[]
|
||||
laeufe Lauf[]
|
||||
|
||||
/// Postgres behandelt NULL als verschieden, deshalb stoeren mehrere manuell
|
||||
/// angelegte Mitarbeiter ohne Quelle und externe Id hier nicht.
|
||||
@@unique([quelleId, externeId])
|
||||
/// Schluesselkandidat fuer den zusammengesetzten Fremdschluessel.
|
||||
@@unique([id, kundeId])
|
||||
@@index([kundeId, aktiv])
|
||||
@@index([abteilung])
|
||||
@@index([upn])
|
||||
@@index([sam])
|
||||
}
|
||||
|
||||
/// Vom Admin gepflegt, Vorschlaege entstehen aus den department-Werten des
|
||||
/// Abgleichs. Liefert beim Onboarding die Ziel-OU und die Standardgruppen.
|
||||
model Abteilung {
|
||||
id String @id @default(cuid())
|
||||
kundeId String
|
||||
name String
|
||||
zielOu String?
|
||||
standardGruppen String[] @default([])
|
||||
mailDomain String?
|
||||
notizen String?
|
||||
archiviert Boolean @default(false)
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||||
laeufe Lauf[]
|
||||
|
||||
@@unique([kundeId, name])
|
||||
/// Schluesselkandidat fuer den zusammengesetzten Fremdschluessel aus der
|
||||
/// Migration, der eine Abteilung eines fremden Kunden ausschliesst.
|
||||
@@unique([id, kundeId])
|
||||
@@index([kundeId, archiviert])
|
||||
}
|
||||
|
||||
// ---- Geraete und Tags ------------------------------------------------------
|
||||
|
||||
/// Nicht nur PCs: ein Serverschrank, ein Raum oder ein Auftrag bekommen
|
||||
/// genauso einen Aufkleber.
|
||||
///
|
||||
/// Ein Geraet ohne registriertAm ist ein vorgedruckter, noch nicht zugeordneter
|
||||
/// Tag. Erst beim ersten Scannen wird es zu einem echten Objekt – deshalb greift
|
||||
/// die Pflicht zu hostname oder bezeichnung erst ab dann (Check-Constraint in
|
||||
/// der Migration).
|
||||
model Geraet {
|
||||
id String @id @default(cuid())
|
||||
kundeId String
|
||||
/// Besitzer bzw. Hauptnutzer. Server, Raeume und Poolgeraete haben keinen.
|
||||
mitarbeiterId String?
|
||||
/// Wird nie geaendert und nie wiederverwendet, auch nicht nach Archivierung.
|
||||
/// Unabhaengig vom Hostname, damit ein Geraet umbenannt werden kann, ohne dass
|
||||
/// der Aufkleber ungueltig wird.
|
||||
tagCode String @unique
|
||||
hostname String?
|
||||
/// Anzeigename fuer Objekte ohne Hostname, etwa "Serverschrank Keller".
|
||||
bezeichnung String?
|
||||
seriennummer String?
|
||||
inventarnummer String?
|
||||
typ GeraeteTyp @default(SONSTIGES)
|
||||
modell String?
|
||||
notizen String?
|
||||
archiviert Boolean @default(false)
|
||||
registriertAm DateTime?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||||
mitarbeiter Mitarbeiter? @relation(fields: [mitarbeiterId], references: [id], onDelete: Restrict)
|
||||
laeufe Lauf[]
|
||||
|
||||
/// Schluesselkandidat fuer den zusammengesetzten Fremdschluessel.
|
||||
@@unique([id, kundeId])
|
||||
@@index([kundeId, archiviert])
|
||||
@@index([mitarbeiterId])
|
||||
@@index([typ])
|
||||
@@index([registriertAm])
|
||||
}
|
||||
|
||||
/// Jeder je vergebene tagCode, unabhaengig davon, ob das Geraet noch existiert.
|
||||
///
|
||||
/// Die Eindeutigkeit allein aus Geraet.tagCode zu ziehen reicht nicht: verschwindet
|
||||
/// die Zeile, waere der Code wieder frei und ein alter Aufkleber im Regal zeigte
|
||||
/// auf ein fremdes Objekt. Aus diesem Buch wird nie geloescht.
|
||||
model TagCodeVergabe {
|
||||
tagCode String @id
|
||||
vergebenAm DateTime @default(now())
|
||||
}
|
||||
|
||||
/// Vorlagen, die auf der Geraeteseite fuer diesen Geraetetyp als Schnellstart
|
||||
/// vorgeschlagen werden. Bewusst ohne kundeId: welcher Schnellstart zu einem
|
||||
/// Geraetetyp passt, haengt am Typ und nicht am Kunden. Kundenspezifische
|
||||
/// Vorlagen filtert die Geraeteseite ueber Vorlage.kundeId heraus.
|
||||
model GeraetetypVorlage {
|
||||
id String @id @default(cuid())
|
||||
geraeteTyp GeraeteTyp
|
||||
vorlageId String
|
||||
position Int @default(0)
|
||||
|
||||
vorlage Vorlage @relation(fields: [vorlageId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([geraeteTyp, vorlageId])
|
||||
@@index([geraeteTyp, position])
|
||||
}
|
||||
|
||||
// ---- Vorlagen --------------------------------------------------------------
|
||||
|
||||
model Vorlage {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String
|
||||
rolle Rolle @default(LESER)
|
||||
oidcSub String? @unique
|
||||
passwortHash String?
|
||||
aktiv Boolean @default(true)
|
||||
beschreibung String?
|
||||
kategorie String?
|
||||
schlagworte String[] @default([])
|
||||
/// Nur im Modus DIENSTLEISTUNG: kundenspezifische Vorlage. Leer heisst
|
||||
/// "gilt fuer alle".
|
||||
kundeId String?
|
||||
archiviert Boolean @default(false)
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
@@index([aktiv])
|
||||
kunde Kunde? @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||||
versionen VorlagenVersion[]
|
||||
typVorgaben GeraetetypVorlage[]
|
||||
|
||||
@@index([archiviert])
|
||||
@@index([kategorie])
|
||||
@@index([kundeId])
|
||||
}
|
||||
|
||||
/// Eine veroeffentlichte Version wird nie wieder veraendert. "Bearbeiten"
|
||||
/// erzeugt eine tiefe Kopie als neuen Entwurf; je Vorlage gibt es hoechstens
|
||||
/// einen Entwurf. Laeufe verweisen immer auf eine veroeffentlichte Version.
|
||||
model VorlagenVersion {
|
||||
id String @id @default(cuid())
|
||||
vorlageId String
|
||||
version Int
|
||||
status VersionStatus @default(ENTWURF)
|
||||
aenderungsnotiz String?
|
||||
veroeffentlichtAm DateTime?
|
||||
veroeffentlichtVonId String?
|
||||
erstelltAm DateTime @default(now())
|
||||
|
||||
vorlage Vorlage @relation(fields: [vorlageId], references: [id], onDelete: Cascade)
|
||||
veroeffentlichtVon Benutzer? @relation("VersionVeroeffentlichtVon", fields: [veroeffentlichtVonId], references: [id], onDelete: Restrict)
|
||||
abschnitte VorlagenAbschnitt[]
|
||||
variablen VorlagenVariable[]
|
||||
laeufe Lauf[]
|
||||
|
||||
@@unique([vorlageId, version])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
/// Wird beim Start eines Laufs abgefragt und fuellt die Platzhalter.
|
||||
model VorlagenVariable {
|
||||
id String @id @default(cuid())
|
||||
versionId String
|
||||
schluessel String
|
||||
beschriftung String
|
||||
typ VariablenTyp @default(TEXT)
|
||||
optionen String[] @default([])
|
||||
pflicht Boolean @default(false)
|
||||
position Int @default(0)
|
||||
|
||||
version VorlagenVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([versionId, schluessel])
|
||||
@@index([versionId, position])
|
||||
}
|
||||
|
||||
model VorlagenAbschnitt {
|
||||
id String @id @default(cuid())
|
||||
versionId String
|
||||
titel String
|
||||
position Int @default(0)
|
||||
|
||||
version VorlagenVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
|
||||
schritte VorlagenSchritt[]
|
||||
|
||||
@@index([versionId, position])
|
||||
}
|
||||
|
||||
model VorlagenSchritt {
|
||||
id String @id @default(cuid())
|
||||
abschnittId String
|
||||
position Int @default(0)
|
||||
titel String
|
||||
/// Markdown, in der Abarbeitungsansicht ausklappbar. Kein rohes HTML.
|
||||
hilfetext String?
|
||||
typ SchrittTyp @default(HAKEN)
|
||||
pflicht Boolean @default(false)
|
||||
/// Typabhaengige Einzelheiten: Auswahloptionen, Min/Max/Einheit, bei BEFEHL
|
||||
/// shell, multiline und der Befehlstext. Ab Phase 4 ausserdem Connector und
|
||||
/// Parameter fuer Schritte vom Typ AKTION.
|
||||
konfig Json @default("{}")
|
||||
dokuUrl String?
|
||||
|
||||
abschnitt VorlagenAbschnitt @relation(fields: [abschnittId], references: [id], onDelete: Cascade)
|
||||
ergebnisse Schrittergebnis[]
|
||||
|
||||
@@index([abschnittId, position])
|
||||
}
|
||||
|
||||
// ---- Laeufe ----------------------------------------------------------------
|
||||
|
||||
model Lauf {
|
||||
id String @id @default(cuid())
|
||||
vorlagenVersionId String
|
||||
titel String
|
||||
kundeId String
|
||||
/// Mitarbeiter aus dem Verzeichnis. Ein Lauf ohne ist gueltig – Server,
|
||||
/// Firewall, Patchday.
|
||||
mitarbeiterId String?
|
||||
/// Freitext, wenn die Person noch nicht im Verzeichnis existiert, etwa beim
|
||||
/// Onboarding. Kann spaeter ueber den UPN mit einem Mitarbeiter verknuepft
|
||||
/// werden; beim Verknuepfen wird dieses Feld geleert, weil eine Bedingung in
|
||||
/// der Datenbank beides zugleich ausschliesst. Der urspruengliche Name bleibt
|
||||
/// im Protokoll erhalten.
|
||||
mitarbeiterName String?
|
||||
geraetId String?
|
||||
/// Die beim Start gewaehlte Abteilung. Speist {{ou}} und {{gruppen}}. Steht als
|
||||
/// Bezug und nicht nur als Wert in variablen, damit die Druckansicht eines
|
||||
/// archivierten Laufs reproduzierbar bleibt, auch wenn die Abteilung spaeter
|
||||
/// umbenannt wird.
|
||||
abteilungId String?
|
||||
ticket String?
|
||||
/// Werte der VorlagenVariablen, beim Start erfasst.
|
||||
variablen Json @default("{}")
|
||||
status LaufStatus @default(OFFEN)
|
||||
zugewiesenAnId String?
|
||||
faelligAm DateTime?
|
||||
/// Leer bei einem Lauf, den die Anwendung selbst angelegt hat – wiederkehrende
|
||||
/// Pruefungen und der Start per Webhook kommen in Phase 4. Kein Codepfad darf
|
||||
/// annehmen, dass hier immer ein Mensch steht.
|
||||
erstelltVonId String?
|
||||
/// Bleibt leer, solange der Lauf nur zugewiesen ist. Wird beim ersten Oeffnen
|
||||
/// zur Bearbeitung gesetzt, zusammen mit dem Wechsel auf IN_ARBEIT.
|
||||
gestartetAm DateTime?
|
||||
abgeschlossenAm DateTime?
|
||||
abgeschlossenVonId String?
|
||||
archiviertAm DateTime?
|
||||
archiviertVonId String?
|
||||
erstelltAm DateTime @default(now())
|
||||
geaendertAm DateTime @updatedAt
|
||||
|
||||
vorlagenVersion VorlagenVersion @relation(fields: [vorlagenVersionId], references: [id], onDelete: Restrict)
|
||||
kunde Kunde @relation(fields: [kundeId], references: [id], onDelete: Restrict)
|
||||
mitarbeiter Mitarbeiter? @relation(fields: [mitarbeiterId], references: [id], onDelete: Restrict)
|
||||
geraet Geraet? @relation(fields: [geraetId], references: [id], onDelete: Restrict)
|
||||
abteilung Abteilung? @relation(fields: [abteilungId], references: [id], onDelete: Restrict)
|
||||
erstelltVon Benutzer? @relation("LaufErstelltVon", fields: [erstelltVonId], references: [id], onDelete: Restrict)
|
||||
zugewiesenAn Benutzer? @relation("LaufZugewiesenAn", fields: [zugewiesenAnId], references: [id], onDelete: Restrict)
|
||||
abgeschlossenVon Benutzer? @relation("LaufAbgeschlossenVon", fields: [abgeschlossenVonId], references: [id], onDelete: Restrict)
|
||||
archiviertVon Benutzer? @relation("LaufArchiviertVon", fields: [archiviertVonId], references: [id], onDelete: Restrict)
|
||||
|
||||
ergebnisse Schrittergebnis[]
|
||||
kommentare Kommentar[]
|
||||
protokolle Protokoll[]
|
||||
|
||||
@@index([kundeId, status])
|
||||
@@index([geraetId, status])
|
||||
@@index([mitarbeiterId])
|
||||
@@index([zugewiesenAnId, status])
|
||||
@@index([status, faelligAm])
|
||||
@@index([vorlagenVersionId])
|
||||
@@index([abteilungId])
|
||||
}
|
||||
|
||||
model Schrittergebnis {
|
||||
id String @id @default(cuid())
|
||||
laufId String
|
||||
vorlagenSchrittId String
|
||||
zustand SchrittZustand @default(OFFEN)
|
||||
/// Der erfasste Wert, je nach Schritt-Typ. Nimmt ab Phase 4 auch das Ergebnis
|
||||
/// einer Aktion auf: Log, IDs, Zeitstempel.
|
||||
wert Json?
|
||||
/// Pflicht bei NICHT_ZUTREFFEND und FEHLGESCHLAGEN, erzwungen im Service.
|
||||
notiz String?
|
||||
aktualisiertVonId String?
|
||||
aktualisiertAm DateTime @updatedAt
|
||||
erstelltAm DateTime @default(now())
|
||||
|
||||
lauf Lauf @relation(fields: [laufId], references: [id], onDelete: Restrict)
|
||||
vorlagenSchritt VorlagenSchritt @relation(fields: [vorlagenSchrittId], references: [id], onDelete: Restrict)
|
||||
/// Leer, wenn der Schritt von der Anwendung selbst gesetzt wurde. Kein
|
||||
/// Codepfad darf annehmen, dass immer ein Mensch dahintersteht.
|
||||
aktualisiertVon Benutzer? @relation(fields: [aktualisiertVonId], references: [id], onDelete: Restrict)
|
||||
kommentare Kommentar[]
|
||||
|
||||
@@unique([laufId, vorlagenSchrittId])
|
||||
@@index([laufId, zustand])
|
||||
}
|
||||
|
||||
model Kommentar {
|
||||
id String @id @default(cuid())
|
||||
laufId String
|
||||
schrittergebnisId String?
|
||||
benutzerId String
|
||||
rumpf String
|
||||
erstelltAm DateTime @default(now())
|
||||
|
||||
lauf Lauf @relation(fields: [laufId], references: [id], onDelete: Restrict)
|
||||
schrittergebnis Schrittergebnis? @relation(fields: [schrittergebnisId], references: [id], onDelete: Restrict)
|
||||
benutzer Benutzer @relation(fields: [benutzerId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@index([laufId, erstelltAm])
|
||||
@@index([schrittergebnisId])
|
||||
}
|
||||
|
||||
// ---- Protokoll und Sperren -------------------------------------------------
|
||||
|
||||
/// Jede Aenderung an einem Schrittergebnis, jeder Statuswechsel eines Laufs und
|
||||
/// jedes Veroeffentlichen einer Vorlage landet hier. Wird nie geloescht.
|
||||
model Protokoll {
|
||||
id String @id @default(cuid())
|
||||
laufId String?
|
||||
entitaet String
|
||||
entitaetId String
|
||||
aktion String
|
||||
benutzerId String?
|
||||
vorher Json?
|
||||
nachher Json?
|
||||
erstelltAm DateTime @default(now())
|
||||
|
||||
lauf Lauf? @relation(fields: [laufId], references: [id], onDelete: Restrict)
|
||||
benutzer Benutzer? @relation(fields: [benutzerId], references: [id], onDelete: Restrict)
|
||||
|
||||
@@index([laufId, erstelltAm])
|
||||
@@index([entitaet, entitaetId])
|
||||
@@index([erstelltAm])
|
||||
}
|
||||
|
||||
/// Sperre fuer Aufgaben, die nur einmal gleichzeitig laufen duerfen – zuerst der
|
||||
/// LDAP-Abgleich. Liegt in der Datenbank, damit auch mehrere Instanzen der
|
||||
/// Anwendung sich einig sind.
|
||||
model AbgleichSperre {
|
||||
schluessel String @id
|
||||
gehaltenVon String
|
||||
gehaltenSeit DateTime @default(now())
|
||||
laeuftAbAm DateTime
|
||||
|
||||
@@index([laeuftAbAm])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user