feat(quick-260923-ad9): Datenmodell, Migration und Reiter-Grundlage - Task 1

Neues Modell Dashboard (D-01/D-02/D-09): position statt Standard-Feld,
kein Unique auf (userId, position) - Umsortieren schreibt spaeter alle
Positionen einer Transaktion neu. WidgetInstance/DashboardLayout haengen
jetzt am Reiter statt am Benutzer (DashboardLayout.dashboardId @unique
ersetzt userId @unique).

Migration 20260923120000_dashboard_tabs: Zeilenschutz mit Mandant- UND
Benutzerdimension (Form 20260911120000/20260921120000), Bestands-
uebernahme fuer jeden Benutzer mit Kacheln oder Anordnung VOR den
Fremdschluesseln (D-03) - gemessen: 0 Kacheln/Anordnungen ohne Reiter,
genau 2 Reiter auf Position 0.

dashboard.service.ts: listDashboards() (Transaktionssperre gegen
doppelte Erstanlage, T-AD9-07), Riegel assertOwnedDashboard() (fail-
closed gegen fremde Reiter, T-AD9-01/02/03) - getLayout/saveLayout/
getWidgets/addWidget laufen jetzt ueber dashboardId statt userId.
GET /dashboard/tabs neu; die vier bestehenden Wege reichen die Reiter-
Kennung durch. Verhalten fuer den Benutzer unveraendert (ein Reiter,
wie bisher) - Task 2 ergaenzt Anlegen/Umbenennen/Loeschen/Umsortieren.

dashboard.service.spec.ts: 43 Tests (31 alte unveraendert + 12 neue fuer
Reiter-Anlage, -Reihenfolge und den Fremdreiter-Riegel bei allen vier
Wegen). Zugriffsklassifikation nachgerechnet: 75 Paare (+1), Bereich
dashboard 21->24 gebunden.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 07:56:38 +02:00
parent 84fe73e16a
commit 9c518238f5
9 changed files with 627 additions and 94 deletions
+42 -6
View File
@@ -187,14 +187,45 @@ model ModuleGrant {
@@index([moduleId])
}
model DashboardLayout {
id String @id @default(uuid())
userId String @unique
// Dashboard-Reiter (quick-260923-ad9, D-01/D-02/D-09): mehrere Dashboards je
// Benutzer, ueber `position` (Integer) aufsteigend sortiert. KEIN Unique auf
// (userId, position) — `reorderDashboards` (Muster FavoritesService.reorder)
// schreibt beim Umsortieren ALLE Positionen eines Benutzers in EINER
// Transaktion neu; ein Unique waere dabei nur im Weg (kollidiert waehrend
// des Umschreibens mit sich selbst). KEIN eigenes Standard-Feld: "als
// Favorit festlegen" IST das Nach-vorn-Ziehen (D-09) — Position 0 ist der
// Standard, es gibt keine zweite Wahrheit daneben. Keine Relation zu
// User/Tenant — Form der Nachbarmodelle WidgetInstance/DashboardImage (eine
// Relation zu User wuerde an Bestandszeilen verwaister Benutzer scheitern).
model Dashboard {
id String @id @default(uuid())
userId String
tenantId String
layouts Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
position Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
widgets WidgetInstance[]
layout DashboardLayout?
@@index([userId])
@@index([tenantId])
}
model DashboardLayout {
id String @id @default(uuid())
userId String
tenantId String
// quick-260923-ad9 (D-02): haengt jetzt am Dashboard statt am Benutzer —
// die Eindeutigkeit wandert von userId auf dashboardId, userId/tenantId
// bleiben fuer Besitz- und Mandantenpruefung erhalten.
dashboardId String @unique
dashboard Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade)
layouts Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([tenantId])
}
@@ -202,6 +233,10 @@ model WidgetInstance {
id String @id @default(uuid())
userId String
tenantId String
// quick-260923-ad9 (D-02): Kacheln haengen ab jetzt am Reiter, nicht mehr
// nur am Benutzer.
dashboardId String
dashboard Dashboard @relation(fields: [dashboardId], references: [id], onDelete: Cascade)
widgetType String
config Json @default("{}")
createdAt DateTime @default(now())
@@ -210,6 +245,7 @@ model WidgetInstance {
@@index([userId])
@@index([tenantId])
@@index([dashboardId])
}
// Bilderrahmen-Widget (quick-260921-pi9): hochgeladene Bilder eines Benutzers.