feat(15-01): Group/GroupMembership/ModuleGrant schema + D-06 backfill migration
- Group/GroupMembership/ModuleGrant models plus MembershipSource enum (D-05), placed under TenantModuleActivation with German block comment - Hand-SQL appended to the generated migration: partial unique index for one default group per tenant (D-13), CHECK num_nonnulls xor-constraint plus two partial unique indexes for ModuleGrant (D-04), and the D-06 backfill (Group -> GroupMembership -> ModuleGrant, each INSERT guarded by WHERE NOT EXISTS for idempotent re-runs on `prisma migrate deploy`) - apps/api/src/groups/migration-sql.spec.ts verifies the hand-SQL by reading migration.sql directly, no DB required - Verified against the local DB: default-group count matches tenant count, membership/grant counts match existing users/active activations, and the XOR constraint rejects a group+user-less insert
This commit is contained in:
@@ -16,6 +16,8 @@ model Tenant {
|
||||
updatedAt DateTime @updatedAt
|
||||
users User[]
|
||||
ldapConfig LdapConfig?
|
||||
groups Group[]
|
||||
moduleGrants ModuleGrant[]
|
||||
}
|
||||
|
||||
enum Role {
|
||||
@@ -42,6 +44,8 @@ model User {
|
||||
avatarPath String?
|
||||
accentColor String?
|
||||
passwordResetTokens PasswordResetToken[]
|
||||
groupMemberships GroupMembership[]
|
||||
moduleGrants ModuleGrant[]
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([username])
|
||||
@@ -102,6 +106,7 @@ model Module {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
activations TenantModuleActivation[]
|
||||
grants ModuleGrant[]
|
||||
}
|
||||
|
||||
model TenantModuleActivation {
|
||||
@@ -116,6 +121,69 @@ model TenantModuleActivation {
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
// Phase 15 (PERM-04/05/06) — zweites Standbein der Zugriffskontrolle neben
|
||||
// TenantModuleActivation. D-05: Gruppen sind Tessera-eigene Objekte pro
|
||||
// Mandant mit optionaler AD-Bindung (ldapDn) — ein Umbau nach Vergabe
|
||||
// echter Freigaben ist eine Datenmigration (one-way). D-13: pro Mandant
|
||||
// darf höchstens eine Gruppe die Standard-Markierung tragen, DB-erzwungen
|
||||
// über einen partiellen Unique-Index in der Hand-SQL-Ergänzung dieser
|
||||
// Migration (Prisma 6.19 kennt keine partiellen Indizes ohne Preview-Flag).
|
||||
// D-04: ModuleGrant trägt bewusst KEIN Rechtestufen-Feld — nur Zugriff an/aus.
|
||||
enum MembershipSource {
|
||||
MANUAL
|
||||
LDAP
|
||||
}
|
||||
|
||||
model Group {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
name String
|
||||
ldapDn String? // optionale AD-Bindung (D-05)
|
||||
isDefault Boolean @default(false) // D-13 — genau eine pro Mandant, DB-erzwungen (Hand-SQL)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
memberships GroupMembership[]
|
||||
grants ModuleGrant[]
|
||||
|
||||
@@unique([tenantId, name]) // Gruppennamen sind pro Mandant eindeutig
|
||||
@@unique([tenantId, ldapDn]) // NULL ist in Postgres je Zeile distinct — mehrere ungebundene Gruppen sind erlaubt
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
model GroupMembership {
|
||||
id String @id @default(uuid())
|
||||
groupId String
|
||||
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
source MembershipSource @default(MANUAL)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([groupId, userId]) // Upsert-Ziel
|
||||
@@index([userId])
|
||||
@@index([groupId])
|
||||
}
|
||||
|
||||
model ModuleGrant {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id])
|
||||
moduleId String
|
||||
module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
||||
groupId String?
|
||||
group Group? @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||
userId String?
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Entweder-oder (Gruppe XOR Benutzer, D-04) + Duplikat-Schutz je Variante
|
||||
// werden per hand-editierter migration.sql ergänzt — Prisma 6.19 hat kein
|
||||
// stabiles partial-index-Feature ohne previewFeatures-Flag.
|
||||
@@index([tenantId])
|
||||
@@index([moduleId])
|
||||
}
|
||||
|
||||
model DashboardLayout {
|
||||
id String @id @default(uuid())
|
||||
userId String @unique
|
||||
|
||||
Reference in New Issue
Block a user