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:
@@ -0,0 +1,113 @@
|
||||
-- 260923-ad9 — Dashboard-Reiter: mehrere Dashboards je Benutzer.
|
||||
--
|
||||
-- Zweck: das Dashboard traegt heute genau eine Kachelflaeche je Benutzer.
|
||||
-- Diese Migration gibt jedem Benutzer mehrere Dashboards ("Reiter"), die
|
||||
-- oben nebeneinander stehen: jeder Reiter mit eigenen Kacheln und eigener
|
||||
-- Anordnung, per Ziehen umsortierbar.
|
||||
--
|
||||
-- D-01: `position` (Integer) traegt die Reihenfolge, aufsteigend sortiert.
|
||||
-- KEIN Unique auf (userId, position) — beim Umsortieren werden alle
|
||||
-- Positionen eines Benutzers in EINER Transaktion neu geschrieben
|
||||
-- (dashboard.service.ts, reorderDashboards, Muster FavoritesService.reorder);
|
||||
-- ein Unique waere dabei nur im Weg.
|
||||
--
|
||||
-- D-03: niemand verliert etwas. Fuer jeden Benutzer, der heute Kacheln ODER
|
||||
-- eine gespeicherte Anordnung hat, entsteht genau EIN Dashboard mit
|
||||
-- position = 0 und dem Namen "Dashboard"; vorhandene Kacheln und die
|
||||
-- vorhandene Anordnung werden darauf umgehaengt. Diese Bestandsuebernahme
|
||||
-- MUSS vor den Fremdschluesseln laufen, sonst scheitert sie an genau diesen
|
||||
-- — deshalb steht sie unten vor den ALTER-TABLE-Schritten fuer
|
||||
-- WidgetInstance/DashboardLayout.
|
||||
--
|
||||
-- D-04: Zeilenschutz ist Pflicht. Die neue Tabelle traegt `tenantId` und
|
||||
-- dieselbe Regel wie ihre Nachbarn — Mandant UND Benutzerdimension von
|
||||
-- Anfang an (Form aus 20260911120000_rls_user_dimension_personal_tables,
|
||||
-- uebernommen aus 20260921120000_dashboard_image).
|
||||
--
|
||||
-- Rechte fuer die Anwendungsrolle tessera_app kommen ueber ALTER DEFAULT
|
||||
-- PRIVILEGES aus 20260909130000_rls_app_role automatisch — hier nichts zu
|
||||
-- tun.
|
||||
--
|
||||
-- WICHTIG: wie alle bisherigen RLS-Migrationen wirkt die Regel erst, wenn
|
||||
-- die Anwendung als Rolle ohne Umgehungsrecht verbindet (Schalter heute AUS,
|
||||
-- siehe docs/mandantentrennung-datenbankrolle.md).
|
||||
|
||||
-- 1) Tabelle Dashboard anlegen, Indizes auf userId und tenantId.
|
||||
CREATE TABLE "Dashboard" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"position" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Dashboard_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "Dashboard_userId_idx" ON "Dashboard"("userId");
|
||||
CREATE INDEX "Dashboard_tenantId_idx" ON "Dashboard"("tenantId");
|
||||
|
||||
-- 2) Zeilenschutz: Mandant UND Benutzer (Muster 20260911120000/20260921120000).
|
||||
ALTER TABLE "Dashboard" ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE "Dashboard" FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation_policy ON "Dashboard"
|
||||
USING (
|
||||
"tenantId" = current_tenant_id()
|
||||
AND (current_user_id() IS NULL OR "userId" = current_user_id())
|
||||
);
|
||||
|
||||
-- 3) Bestandsuebernahme (D-03): je Benutzer aus der Vereinigung der
|
||||
-- Benutzer mit Kacheln und der Benutzer mit gespeicherter Anordnung genau
|
||||
-- EINE Zeile einfuegen. DISTINCT ON sichert "je Benutzer genau eine Zeile"
|
||||
-- auch fuer den theoretischen Fall "derselbe Benutzer mit zwei
|
||||
-- Mandantenkennungen" ab (deterministische Wahl ueber die Sortierung nach
|
||||
-- tenantId als zweitem Kriterium).
|
||||
INSERT INTO "Dashboard" ("id", "userId", "tenantId", "name", "position", "createdAt", "updatedAt")
|
||||
SELECT gen_random_uuid(), bestand."userId", bestand."tenantId", 'Dashboard', 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
FROM (
|
||||
SELECT DISTINCT ON ("userId") "userId", "tenantId"
|
||||
FROM (
|
||||
SELECT "userId", "tenantId" FROM "WidgetInstance"
|
||||
UNION ALL
|
||||
SELECT "userId", "tenantId" FROM "DashboardLayout"
|
||||
) AS vereinigung
|
||||
ORDER BY "userId", "tenantId"
|
||||
) AS bestand;
|
||||
|
||||
-- 4) WidgetInstance.dashboardId: zunaechst NULLbar ergaenzen, aus der neuen
|
||||
-- Tabelle ueber die Benutzerkennung befuellen (fuer jeden Benutzer mit
|
||||
-- Kacheln existiert nach Schritt 3 GENAU ein Dashboard), dann NOT NULL,
|
||||
-- Index, Fremdschluessel mit Loeschweitergabe.
|
||||
ALTER TABLE "WidgetInstance" ADD COLUMN "dashboardId" TEXT;
|
||||
|
||||
UPDATE "WidgetInstance" wi
|
||||
SET "dashboardId" = d."id"
|
||||
FROM "Dashboard" d
|
||||
WHERE d."userId" = wi."userId";
|
||||
|
||||
ALTER TABLE "WidgetInstance" ALTER COLUMN "dashboardId" SET NOT NULL;
|
||||
CREATE INDEX "WidgetInstance_dashboardId_idx" ON "WidgetInstance"("dashboardId");
|
||||
ALTER TABLE "WidgetInstance" ADD CONSTRAINT "WidgetInstance_dashboardId_fkey"
|
||||
FOREIGN KEY ("dashboardId") REFERENCES "Dashboard"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- 5) DashboardLayout.dashboardId: dieselbe Uebernahme; zusaetzlich die
|
||||
-- Eindeutigkeit auf userId entfernen (mehrere Reiter je Benutzer sind jetzt
|
||||
-- erlaubt), dort einen gewoehnlichen Index anlegen, und die Eindeutigkeit
|
||||
-- auf dashboardId anlegen (ein Reiter hat hoechstens eine gespeicherte
|
||||
-- Anordnung).
|
||||
ALTER TABLE "DashboardLayout" ADD COLUMN "dashboardId" TEXT;
|
||||
|
||||
UPDATE "DashboardLayout" dl
|
||||
SET "dashboardId" = d."id"
|
||||
FROM "Dashboard" d
|
||||
WHERE d."userId" = dl."userId";
|
||||
|
||||
ALTER TABLE "DashboardLayout" ALTER COLUMN "dashboardId" SET NOT NULL;
|
||||
|
||||
DROP INDEX "DashboardLayout_userId_key";
|
||||
CREATE INDEX "DashboardLayout_userId_idx" ON "DashboardLayout"("userId");
|
||||
|
||||
CREATE UNIQUE INDEX "DashboardLayout_dashboardId_key" ON "DashboardLayout"("dashboardId");
|
||||
ALTER TABLE "DashboardLayout" ADD CONSTRAINT "DashboardLayout_dashboardId_fkey"
|
||||
FOREIGN KEY ("dashboardId") REFERENCES "Dashboard"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
Reference in New Issue
Block a user