feat(quick-261003-387): Modulkategorien je Organisation - Tabellen, Grundbestand, Lese-Endpunkt bis in die Seitenleiste

- Tabellen ModuleCategory und ModuleCategoryPlacement mit Zeilenschutz, Spalte CustomModule.sortOrder
- Dienst legt den Grundbestand beim ersten Lesen an, legt die wirksame Kategorie ueber Modullisten
- GET /module-categories, PATCH :key (nur Administratoren), /modules/active liefert wirksame Kategorie
- Seitenleiste ordnet Gruppen und Eintraege nach Kategorienspeicher, zeigt umbenannte Namen

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-03 02:33:59 +02:00
parent 53a49a109f
commit 8ec116c75a
21 changed files with 1052 additions and 16 deletions
@@ -0,0 +1,89 @@
-- quick-261003-387 — Modulkategorien durch Administratoren bearbeitbar.
--
-- Zweck: zwei neue Tabellen und eine neue Spalte.
-- * "ModuleCategory": die Kategorien einer Organisation (Kennung, optionaler
-- eigener Name, Reihenfolge, Systemkennzeichen fuer "Eigene Module").
-- Die Kennung ist unveraenderlich, sie steht als URL-Segment in
-- /modules/<kennung>/<slug>.
-- * "ModuleCategoryPlacement": Zuordnung eines Marktplatz-Moduls zu einer
-- Kategorie der Organisation samt Reihenfolge. Die Spalte "Module"."category"
-- (fuer alle Organisationen gleich) bleibt unveraendert; die wirksame
-- Kategorie ist die Zuordnung, sonst diese Spalte.
-- * "CustomModule"."sortOrder": Reihenfolge gemeinsamer eigener Module
-- innerhalb ihrer Kategorie (NULL = noch nicht sortiert).
-- Es gibt keine Rueckfuellung in SQL: der Dienst legt den Grundbestand je
-- Organisation beim ersten Lesen an.
--
-- Von Hand geschrieben (Vorbild 20261002150000_nextcloud_status), von Hand
-- gepflegter Kopfkommentar Pflicht bei jeder RLS-Migration in diesem Projekt.
--
-- Zeilenschutz (Pflicht — sonst schlaegt rls-coverage.spec.ts fehl): beide
-- Tabellen tragen `tenantId` und `tenant_isolation_policy` OHNE
-- Benutzerdimension (`USING ("tenantId" = current_tenant_id())`) — die
-- Kategorien sind gemeinsame Einstellungen der Organisation, nicht
-- persoenliche Daten eines Benutzers. Keine `system_read_policy`: es gibt
-- keinen Hintergrunddienst, der darueber liest.
--
-- Rechte fuer die Anwendungsrolle tessera_app kommen automatisch ueber
-- ALTER DEFAULT PRIVILEGES aus 20260909130000_rls_app_role — hier nichts zu
-- tun.
--
-- WICHTIG: wie alle bisherigen RLS-Migrationen wirken diese Regeln erst,
-- wenn die Anwendung als Rolle ohne Umgehungsrecht verbindet (Schalter
-- heute AUS, siehe docs/mandantentrennung-datenbankrolle.md).
-- AlterTable
ALTER TABLE "CustomModule" ADD COLUMN "sortOrder" INTEGER;
-- CreateTable
CREATE TABLE "ModuleCategory" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"key" TEXT NOT NULL,
"name" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"isSystem" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ModuleCategory_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ModuleCategoryPlacement" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"moduleId" TEXT NOT NULL,
"categoryKey" TEXT NOT NULL,
"sortOrder" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ModuleCategoryPlacement_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "ModuleCategory_tenantId_idx" ON "ModuleCategory"("tenantId");
-- CreateIndex
CREATE UNIQUE INDEX "ModuleCategory_tenantId_key_key" ON "ModuleCategory"("tenantId", "key");
-- CreateIndex
CREATE INDEX "ModuleCategoryPlacement_tenantId_idx" ON "ModuleCategoryPlacement"("tenantId");
-- CreateIndex
CREATE UNIQUE INDEX "ModuleCategoryPlacement_tenantId_moduleId_key" ON "ModuleCategoryPlacement"("tenantId", "moduleId");
-- AddForeignKey
ALTER TABLE "ModuleCategoryPlacement" ADD CONSTRAINT "ModuleCategoryPlacement_moduleId_fkey" FOREIGN KEY ("moduleId") REFERENCES "Module"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- Zeilenschutz
ALTER TABLE "ModuleCategory" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "ModuleCategory" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "ModuleCategory"
USING ("tenantId" = current_tenant_id());
ALTER TABLE "ModuleCategoryPlacement" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "ModuleCategoryPlacement" FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_policy ON "ModuleCategoryPlacement"
USING ("tenantId" = current_tenant_id());