-- 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;