Workspace und Datenmodell

pnpm-Workspace mit web/, worker/, pdf/ und packages/db. Das Datenbankpaket ist
eigenständig, weil Web-App und Worker denselben Prisma-Client brauchen.

Schema nach docs/04-datenmodell.md, mit drei Abweichungen, die dort begründet
sind: CaseParticipant bekommt einen eigenen Schlüssel statt eines
zusammengesetzten mit NULL-Spalten, die Aktenzeichen laufen über CaseCounter
statt über eine Sequenz pro Jahr, und ServiceHeartbeat kommt dazu, damit
/api/health den Gateway-Zustand kennt, ohne Discord zu fragen.

Die zweite Migration enthält, was Prisma nicht ausdrücken kann:
- kein DELETE auf CaseEntry, CaseEntryVersion und AuditLog
- authorId, createdAt, caseId und type eines Eintrags sind unveränderlich
- an einer Eintragsversion darf nur minClearance angehoben werden
- Volltextsuche mit deutscher Konfiguration; geschwärzte Bereiche werden vor
  dem Indizieren entfernt, sonst verrät ein Treffer den Inhalt
- pg_notify bei Rollen- und Mapping-Änderungen

Acht Tests prüfen diese Regeln gegen eine echte Postgres-Instanz, nicht gegen
die Anwendungslogik.
This commit is contained in:
Harlekin
2026-09-01 00:01:29 +02:00
parent 72c47e74a6
commit 8f0644e053
20 changed files with 9085 additions and 0 deletions
@@ -0,0 +1,557 @@
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
-- CreateEnum
CREATE TYPE "NotifyMode" AS ENUM ('ALL', 'OWN_CASES', 'MENTIONS', 'OFF');
-- CreateEnum
CREATE TYPE "CaseStatus" AS ENUM ('DRAFT', 'OPEN', 'DORMANT', 'REVIEW', 'CLOSED', 'ARCHIVED', 'SEALED');
-- CreateEnum
CREATE TYPE "EntryType" AS ENUM ('NOTE', 'INTERROGATION', 'EVIDENCE', 'STATUS_CHANGE', 'CORRECTION', 'SYSTEM');
-- CreateEnum
CREATE TYPE "EditPolicy" AS ENUM ('APPEND_ONLY', 'GRACE_WINDOW', 'OPEN');
-- CreateEnum
CREATE TYPE "ParticipantRole" AS ENUM ('LEAD', 'INVESTIGATOR', 'WITNESS', 'SUBJECT', 'OBSERVER');
-- CreateEnum
CREATE TYPE "LinkKind" AS ENUM ('RELATED', 'SUCCESSOR', 'MERGED_INTO');
-- CreateEnum
CREATE TYPE "ScanStatus" AS ENUM ('PENDING', 'CLEAN', 'BLOCKED', 'FAILED');
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"username" TEXT NOT NULL,
"displayName" TEXT NOT NULL,
"avatarHash" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"firstSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"notifyMode" "NotifyMode" NOT NULL DEFAULT 'OWN_CASES',
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "UserRole" (
"userId" TEXT NOT NULL,
"discordRoleId" TEXT NOT NULL,
CONSTRAINT "UserRole_pkey" PRIMARY KEY ("userId","discordRoleId")
);
-- CreateTable
CREATE TABLE "RoleMapping" (
"discordRoleId" TEXT NOT NULL,
"label" TEXT NOT NULL,
"clearanceLevel" INTEGER NOT NULL DEFAULT 0,
"permissions" TEXT[],
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "RoleMapping_pkey" PRIMARY KEY ("discordRoleId")
);
-- CreateTable
CREATE TABLE "ClearanceLevelConfig" (
"level" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"hiddenBelow" BOOLEAN NOT NULL DEFAULT false,
"colorToken" TEXT NOT NULL,
CONSTRAINT "ClearanceLevelConfig_pkey" PRIMARY KEY ("level")
);
-- CreateTable
CREATE TABLE "Session" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expiresAt" TIMESTAMP(3) NOT NULL,
"lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userAgent" TEXT,
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CaseTemplate" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"fields" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdById" TEXT,
CONSTRAINT "CaseTemplate_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Case" (
"id" TEXT NOT NULL,
"caseNumber" TEXT NOT NULL,
"title" TEXT NOT NULL,
"status" "CaseStatus" NOT NULL DEFAULT 'DRAFT',
"classification" INTEGER NOT NULL DEFAULT 1,
"templateId" TEXT NOT NULL,
"templateKey" TEXT NOT NULL,
"dueAt" TIMESTAMP(3),
"closedAt" TIMESTAMP(3),
"outcome" TEXT,
"createdById" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"searchVector" tsvector,
CONSTRAINT "Case_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CaseCounter" (
"year" INTEGER NOT NULL,
"lastNumber" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "CaseCounter_pkey" PRIMARY KEY ("year")
);
-- CreateTable
CREATE TABLE "CaseSection" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"fieldKey" TEXT NOT NULL,
"minClearance" INTEGER NOT NULL DEFAULT 0,
"currentVersion" INTEGER NOT NULL DEFAULT 1,
"value" JSONB NOT NULL,
"updatedAt" TIMESTAMP(3) NOT NULL,
"updatedById" TEXT NOT NULL,
CONSTRAINT "CaseSection_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CaseSectionVersion" (
"id" TEXT NOT NULL,
"sectionId" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"value" JSONB NOT NULL,
"changeReason" TEXT,
"authorId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CaseSectionVersion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CaseEntry" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"type" "EntryType" NOT NULL,
"body" TEXT NOT NULL,
"minClearance" INTEGER NOT NULL DEFAULT 0,
"authorId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"correctsId" TEXT,
"version" INTEGER NOT NULL DEFAULT 1,
"editedAt" TIMESTAMP(3),
"editedById" TEXT,
"editVisible" BOOLEAN NOT NULL DEFAULT false,
"retractedAt" TIMESTAMP(3),
"retractedById" TEXT,
"retractionReason" TEXT,
CONSTRAINT "CaseEntry_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CaseEntryVersion" (
"id" TEXT NOT NULL,
"entryId" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"body" TEXT NOT NULL,
"minClearance" INTEGER NOT NULL,
"changeReason" TEXT,
"editedById" TEXT NOT NULL,
"editedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CaseEntryVersion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AppSetting" (
"id" INTEGER NOT NULL DEFAULT 1,
"entryEditPolicy" "EditPolicy" NOT NULL DEFAULT 'GRACE_WINDOW',
"entryEditGraceMinutes" INTEGER NOT NULL DEFAULT 15,
"entryEditMarkGrace" BOOLEAN NOT NULL DEFAULT false,
"updatedAt" TIMESTAMP(3) NOT NULL,
"updatedById" TEXT,
CONSTRAINT "AppSetting_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CaseParticipant" (
"id" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
"userId" TEXT,
"personnelFileId" TEXT,
"role" "ParticipantRole" NOT NULL,
"addedById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CaseParticipant_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CaseExclusion" (
"caseId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"reason" TEXT,
"addedById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CaseExclusion_pkey" PRIMARY KEY ("caseId","userId")
);
-- CreateTable
CREATE TABLE "CaseLink" (
"fromId" TEXT NOT NULL,
"toId" TEXT NOT NULL,
"kind" "LinkKind" NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CaseLink_pkey" PRIMARY KEY ("fromId","toId","kind")
);
-- CreateTable
CREATE TABLE "PersonnelFile" (
"id" TEXT NOT NULL,
"userId" TEXT,
"displayName" TEXT NOT NULL,
"serviceNumber" TEXT,
"rank" TEXT,
"department" TEXT,
"status" TEXT,
"classification" INTEGER NOT NULL DEFAULT 1,
"fields" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"searchVector" tsvector,
CONSTRAINT "PersonnelFile_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Attachment" (
"id" TEXT NOT NULL,
"entryId" TEXT,
"caseId" TEXT,
"originalName" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"sizeBytes" INTEGER NOT NULL,
"sha256" TEXT NOT NULL,
"scanStatus" "ScanStatus" NOT NULL DEFAULT 'PENDING',
"scanMessage" TEXT,
"storageKey" TEXT NOT NULL,
"thumbKey" TEXT,
"blurhash" TEXT,
"uploadedById" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Attachment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Channel" (
"id" TEXT NOT NULL,
"key" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"minClearance" INTEGER NOT NULL DEFAULT 0,
"isArchived" BOOLEAN NOT NULL DEFAULT false,
"createdById" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Channel_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ChatMessage" (
"id" TEXT NOT NULL,
"caseId" TEXT,
"channelId" TEXT,
"authorId" TEXT NOT NULL,
"body" TEXT NOT NULL,
"replyToId" TEXT,
"editedAt" TIMESTAMP(3),
"withdrawnAt" TIMESTAMP(3),
"adoptedEntryId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ChatMessage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ChatReaction" (
"messageId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"key" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ChatReaction_pkey" PRIMARY KEY ("messageId","userId","key")
);
-- CreateTable
CREATE TABLE "ReadMarker" (
"userId" TEXT NOT NULL,
"scopeType" TEXT NOT NULL,
"scopeId" TEXT NOT NULL,
"lastReadAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ReadMarker_pkey" PRIMARY KEY ("userId","scopeType","scopeId")
);
-- CreateTable
CREATE TABLE "Watch" (
"userId" TEXT NOT NULL,
"caseId" TEXT NOT NULL,
CONSTRAINT "Watch_pkey" PRIMARY KEY ("userId","caseId")
);
-- CreateTable
CREATE TABLE "ServiceHeartbeat" (
"service" TEXT NOT NULL,
"status" TEXT NOT NULL,
"detail" TEXT,
"at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ServiceHeartbeat_pkey" PRIMARY KEY ("service")
);
-- CreateTable
CREATE TABLE "AuditLog" (
"id" BIGSERIAL NOT NULL,
"at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"actorId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"targetType" TEXT NOT NULL,
"targetId" TEXT NOT NULL,
"clearanceAtTime" INTEGER NOT NULL,
"meta" JSONB,
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "User_isActive_displayName_idx" ON "User"("isActive", "displayName");
-- CreateIndex
CREATE INDEX "UserRole_discordRoleId_idx" ON "UserRole"("discordRoleId");
-- CreateIndex
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
-- CreateIndex
CREATE INDEX "Session_expiresAt_idx" ON "Session"("expiresAt");
-- CreateIndex
CREATE INDEX "CaseTemplate_key_isActive_idx" ON "CaseTemplate"("key", "isActive");
-- CreateIndex
CREATE UNIQUE INDEX "CaseTemplate_key_version_key" ON "CaseTemplate"("key", "version");
-- CreateIndex
CREATE UNIQUE INDEX "Case_caseNumber_key" ON "Case"("caseNumber");
-- CreateIndex
CREATE INDEX "Case_status_updatedAt_idx" ON "Case"("status", "updatedAt" DESC);
-- CreateIndex
CREATE INDEX "Case_classification_idx" ON "Case"("classification");
-- CreateIndex
CREATE INDEX "Case_dueAt_idx" ON "Case"("dueAt");
-- CreateIndex
CREATE INDEX "Case_createdById_idx" ON "Case"("createdById");
-- CreateIndex
CREATE UNIQUE INDEX "CaseSection_caseId_fieldKey_key" ON "CaseSection"("caseId", "fieldKey");
-- CreateIndex
CREATE INDEX "CaseSectionVersion_sectionId_version_idx" ON "CaseSectionVersion"("sectionId", "version" DESC);
-- CreateIndex
CREATE UNIQUE INDEX "CaseSectionVersion_sectionId_version_key" ON "CaseSectionVersion"("sectionId", "version");
-- CreateIndex
CREATE INDEX "CaseEntry_caseId_createdAt_idx" ON "CaseEntry"("caseId", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "CaseEntry_caseId_editedAt_idx" ON "CaseEntry"("caseId", "editedAt" DESC);
-- CreateIndex
CREATE INDEX "CaseEntry_authorId_idx" ON "CaseEntry"("authorId");
-- CreateIndex
CREATE INDEX "CaseEntry_correctsId_idx" ON "CaseEntry"("correctsId");
-- CreateIndex
CREATE INDEX "CaseEntryVersion_entryId_version_idx" ON "CaseEntryVersion"("entryId", "version" DESC);
-- CreateIndex
CREATE UNIQUE INDEX "CaseEntryVersion_entryId_version_key" ON "CaseEntryVersion"("entryId", "version");
-- CreateIndex
CREATE INDEX "CaseParticipant_caseId_idx" ON "CaseParticipant"("caseId");
-- CreateIndex
CREATE INDEX "CaseParticipant_userId_idx" ON "CaseParticipant"("userId");
-- CreateIndex
CREATE INDEX "CaseParticipant_personnelFileId_idx" ON "CaseParticipant"("personnelFileId");
-- CreateIndex
CREATE INDEX "CaseExclusion_userId_idx" ON "CaseExclusion"("userId");
-- CreateIndex
CREATE INDEX "CaseLink_toId_idx" ON "CaseLink"("toId");
-- CreateIndex
CREATE UNIQUE INDEX "PersonnelFile_serviceNumber_key" ON "PersonnelFile"("serviceNumber");
-- CreateIndex
CREATE INDEX "PersonnelFile_displayName_idx" ON "PersonnelFile"("displayName");
-- CreateIndex
CREATE INDEX "PersonnelFile_userId_idx" ON "PersonnelFile"("userId");
-- CreateIndex
CREATE INDEX "PersonnelFile_classification_idx" ON "PersonnelFile"("classification");
-- CreateIndex
CREATE INDEX "Attachment_sha256_idx" ON "Attachment"("sha256");
-- CreateIndex
CREATE INDEX "Attachment_scanStatus_idx" ON "Attachment"("scanStatus");
-- CreateIndex
CREATE INDEX "Attachment_caseId_idx" ON "Attachment"("caseId");
-- CreateIndex
CREATE INDEX "Attachment_entryId_idx" ON "Attachment"("entryId");
-- CreateIndex
CREATE INDEX "Attachment_uploadedById_createdAt_idx" ON "Attachment"("uploadedById", "createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "Channel_key_key" ON "Channel"("key");
-- CreateIndex
CREATE INDEX "Channel_isArchived_name_idx" ON "Channel"("isArchived", "name");
-- CreateIndex
CREATE INDEX "ChatMessage_caseId_createdAt_idx" ON "ChatMessage"("caseId", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "ChatMessage_channelId_createdAt_idx" ON "ChatMessage"("channelId", "createdAt" DESC);
-- CreateIndex
CREATE INDEX "ChatMessage_authorId_idx" ON "ChatMessage"("authorId");
-- CreateIndex
CREATE INDEX "ChatMessage_replyToId_idx" ON "ChatMessage"("replyToId");
-- CreateIndex
CREATE INDEX "Watch_caseId_idx" ON "Watch"("caseId");
-- CreateIndex
CREATE INDEX "AuditLog_targetType_targetId_at_idx" ON "AuditLog"("targetType", "targetId", "at" DESC);
-- CreateIndex
CREATE INDEX "AuditLog_actorId_at_idx" ON "AuditLog"("actorId", "at" DESC);
-- CreateIndex
CREATE INDEX "AuditLog_action_at_idx" ON "AuditLog"("action", "at" DESC);
-- AddForeignKey
ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Case" ADD CONSTRAINT "Case_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "CaseTemplate"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseSection" ADD CONSTRAINT "CaseSection_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "Case"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseSectionVersion" ADD CONSTRAINT "CaseSectionVersion_sectionId_fkey" FOREIGN KEY ("sectionId") REFERENCES "CaseSection"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseEntry" ADD CONSTRAINT "CaseEntry_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "Case"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseEntry" ADD CONSTRAINT "CaseEntry_correctsId_fkey" FOREIGN KEY ("correctsId") REFERENCES "CaseEntry"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseEntryVersion" ADD CONSTRAINT "CaseEntryVersion_entryId_fkey" FOREIGN KEY ("entryId") REFERENCES "CaseEntry"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseParticipant" ADD CONSTRAINT "CaseParticipant_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "Case"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseParticipant" ADD CONSTRAINT "CaseParticipant_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseParticipant" ADD CONSTRAINT "CaseParticipant_personnelFileId_fkey" FOREIGN KEY ("personnelFileId") REFERENCES "PersonnelFile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseExclusion" ADD CONSTRAINT "CaseExclusion_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "Case"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseExclusion" ADD CONSTRAINT "CaseExclusion_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseLink" ADD CONSTRAINT "CaseLink_fromId_fkey" FOREIGN KEY ("fromId") REFERENCES "Case"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CaseLink" ADD CONSTRAINT "CaseLink_toId_fkey" FOREIGN KEY ("toId") REFERENCES "Case"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PersonnelFile" ADD CONSTRAINT "PersonnelFile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_entryId_fkey" FOREIGN KEY ("entryId") REFERENCES "CaseEntry"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Attachment" ADD CONSTRAINT "Attachment_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "Case"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ChatMessage" ADD CONSTRAINT "ChatMessage_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "Case"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ChatMessage" ADD CONSTRAINT "ChatMessage_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ChatMessage" ADD CONSTRAINT "ChatMessage_replyToId_fkey" FOREIGN KEY ("replyToId") REFERENCES "ChatMessage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ChatReaction" ADD CONSTRAINT "ChatReaction_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "ChatMessage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ReadMarker" ADD CONSTRAINT "ReadMarker_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Watch" ADD CONSTRAINT "Watch_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Watch" ADD CONSTRAINT "Watch_caseId_fkey" FOREIGN KEY ("caseId") REFERENCES "Case"("id") ON DELETE CASCADE ON UPDATE CASCADE;