feat: Datenmodell für Katalog, Tracking und Universen
Prisma-Schema in vier Schichten: Konten und Zugang, gemeinsamer Katalog (über die TMDB-ID dedupliziert), persönliches Tracking je Nutzer sowie Universen mit mehreren Reihenfolgen. Dazu Benachrichtigungskanäle und Protokoll der Hintergrundläufe. Prisma 7 verlangt einen Treiber-Adapter statt der URL im Schema; die Konfiguration liegt deshalb in prisma.config.mjs — bewusst als .mjs, damit die CLI auch im schlanken Produktionsimage ohne TypeScript-Loader läuft.
This commit is contained in:
@@ -0,0 +1,553 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "Role" AS ENUM ('ADMIN', 'USER');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "TitleType" AS ENUM ('SERIES', 'MOVIE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ProviderKind" AS ENUM ('FLATRATE', 'RENT', 'BUY', 'ADS', 'FREE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "EntryStatus" AS ENUM ('WATCHING', 'PAUSED', 'COMPLETED', 'DROPPED', 'WATCHLIST');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SharePermission" AS ENUM ('READ', 'WRITE');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "OrderKind" AS ENUM ('RELEASE', 'CUSTOM');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ChannelKind" AS ENUM ('EMAIL', 'NTFY', 'GOTIFY', 'WEBHOOK');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"passwordHash" TEXT,
|
||||
"role" "Role" NOT NULL DEFAULT 'USER',
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"region" TEXT NOT NULL DEFAULT 'DE',
|
||||
"locale" TEXT NOT NULL DEFAULT 'de',
|
||||
"publicProfile" BOOLEAN NOT NULL DEFAULT false,
|
||||
"profileSlug" TEXT,
|
||||
"notifyNewEpisode" BOOLEAN NOT NULL DEFAULT true,
|
||||
"notifyRelease" BOOLEAN NOT NULL DEFAULT true,
|
||||
"notifyAvailability" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Session" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tokenHash" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||
"userAgent" TEXT,
|
||||
"ip" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"lastSeen" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Session_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ApiKey" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"prefix" TEXT NOT NULL,
|
||||
"keyHash" TEXT NOT NULL,
|
||||
"lastUsedAt" TIMESTAMP(3),
|
||||
"revokedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "OidcAccount" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"issuer" TEXT NOT NULL,
|
||||
"subject" TEXT NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "OidcAccount_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "AppSetting" (
|
||||
"key" TEXT NOT NULL,
|
||||
"value" JSONB NOT NULL,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "AppSetting_pkey" PRIMARY KEY ("key")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Title" (
|
||||
"id" TEXT NOT NULL,
|
||||
"type" "TitleType" NOT NULL,
|
||||
"tmdbId" INTEGER,
|
||||
"imdbId" TEXT,
|
||||
"title" TEXT NOT NULL,
|
||||
"originalTitle" TEXT,
|
||||
"overview" TEXT,
|
||||
"tagline" TEXT,
|
||||
"posterPath" TEXT,
|
||||
"backdropPath" TEXT,
|
||||
"releaseDate" TIMESTAMP(3),
|
||||
"lastAirDate" TIMESTAMP(3),
|
||||
"runtime" INTEGER,
|
||||
"status" TEXT,
|
||||
"inProduction" BOOLEAN NOT NULL DEFAULT false,
|
||||
"genres" TEXT[],
|
||||
"originalLang" TEXT,
|
||||
"homepage" TEXT,
|
||||
"voteAverage" DOUBLE PRECISION,
|
||||
"seasonCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"episodeCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"tmdbCollectionId" INTEGER,
|
||||
"tmdbCollectionName" TEXT,
|
||||
"syncedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Title_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Season" (
|
||||
"id" TEXT NOT NULL,
|
||||
"titleId" TEXT NOT NULL,
|
||||
"tmdbId" INTEGER,
|
||||
"seasonNumber" INTEGER NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"overview" TEXT,
|
||||
"posterPath" TEXT,
|
||||
"airDate" TIMESTAMP(3),
|
||||
"episodeCount" INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT "Season_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Episode" (
|
||||
"id" TEXT NOT NULL,
|
||||
"titleId" TEXT NOT NULL,
|
||||
"seasonId" TEXT NOT NULL,
|
||||
"tmdbId" INTEGER,
|
||||
"seasonNumber" INTEGER NOT NULL,
|
||||
"episodeNumber" INTEGER NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"overview" TEXT,
|
||||
"stillPath" TEXT,
|
||||
"airDate" TIMESTAMP(3),
|
||||
"runtime" INTEGER,
|
||||
|
||||
CONSTRAINT "Episode_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WatchProvider" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tmdbId" INTEGER NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"logoPath" TEXT,
|
||||
"color" TEXT,
|
||||
|
||||
CONSTRAINT "WatchProvider_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TitleProvider" (
|
||||
"id" TEXT NOT NULL,
|
||||
"titleId" TEXT NOT NULL,
|
||||
"providerId" TEXT NOT NULL,
|
||||
"region" TEXT NOT NULL,
|
||||
"kind" "ProviderKind" NOT NULL,
|
||||
"firstSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "TitleProvider_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "LibraryEntry" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"titleId" TEXT NOT NULL,
|
||||
"status" "EntryStatus" NOT NULL DEFAULT 'WATCHLIST',
|
||||
"rating" INTEGER,
|
||||
"notes" TEXT,
|
||||
"whereToWatch" TEXT,
|
||||
"favorite" BOOLEAN NOT NULL DEFAULT false,
|
||||
"currentRun" INTEGER NOT NULL DEFAULT 0,
|
||||
"startedAt" TIMESTAMP(3),
|
||||
"finishedAt" TIMESTAMP(3),
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "LibraryEntry_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Tag" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"color" TEXT,
|
||||
|
||||
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TagOnEntry" (
|
||||
"tagId" TEXT NOT NULL,
|
||||
"entryId" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "TagOnEntry_pkey" PRIMARY KEY ("tagId","entryId")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Watch" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"entryId" TEXT,
|
||||
"titleId" TEXT NOT NULL,
|
||||
"episodeId" TEXT,
|
||||
"run" INTEGER NOT NULL DEFAULT 0,
|
||||
"watchedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"minutes" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Watch_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Universe" (
|
||||
"id" TEXT NOT NULL,
|
||||
"ownerId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"coverPath" TEXT,
|
||||
"deletedAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Universe_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UniverseMember" (
|
||||
"id" TEXT NOT NULL,
|
||||
"universeId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"permission" "SharePermission" NOT NULL DEFAULT 'READ',
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "UniverseMember_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UniverseItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"universeId" TEXT NOT NULL,
|
||||
"titleId" TEXT NOT NULL,
|
||||
"note" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "UniverseItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UniverseOrder" (
|
||||
"id" TEXT NOT NULL,
|
||||
"universeId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"kind" "OrderKind" NOT NULL DEFAULT 'CUSTOM',
|
||||
"isDefault" BOOLEAN NOT NULL DEFAULT false,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "UniverseOrder_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UniverseOrderItem" (
|
||||
"id" TEXT NOT NULL,
|
||||
"orderId" TEXT NOT NULL,
|
||||
"itemId" TEXT NOT NULL,
|
||||
"position" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "UniverseOrderItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "NotificationChannel" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"kind" "ChannelKind" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"config" JSONB NOT NULL,
|
||||
"enabled" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "NotificationChannel_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "NotificationLog" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"dedupeKey" TEXT NOT NULL,
|
||||
"subject" TEXT NOT NULL,
|
||||
"body" TEXT NOT NULL,
|
||||
"sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"ok" BOOLEAN NOT NULL DEFAULT true,
|
||||
"error" TEXT,
|
||||
|
||||
CONSTRAINT "NotificationLog_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SyncRun" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kind" TEXT NOT NULL,
|
||||
"startedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"finishedAt" TIMESTAMP(3),
|
||||
"ok" BOOLEAN NOT NULL DEFAULT false,
|
||||
"message" TEXT,
|
||||
"stats" JSONB,
|
||||
|
||||
CONSTRAINT "SyncRun_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_profileSlug_key" ON "User"("profileSlug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_createdAt_idx" ON "User"("createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Session_tokenHash_key" ON "Session"("tokenHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Session_expiresAt_idx" ON "Session"("expiresAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ApiKey_prefix_key" ON "ApiKey"("prefix");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ApiKey_keyHash_key" ON "ApiKey"("keyHash");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ApiKey_userId_idx" ON "ApiKey"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "OidcAccount_userId_idx" ON "OidcAccount"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "OidcAccount_issuer_subject_key" ON "OidcAccount"("issuer", "subject");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Title_type_idx" ON "Title"("type");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Title_releaseDate_idx" ON "Title"("releaseDate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Title_title_idx" ON "Title"("title");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Title_tmdbCollectionId_idx" ON "Title"("tmdbCollectionId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Title_type_tmdbId_key" ON "Title"("type", "tmdbId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Season_titleId_idx" ON "Season"("titleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Season_titleId_seasonNumber_key" ON "Season"("titleId", "seasonNumber");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Episode_titleId_idx" ON "Episode"("titleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Episode_seasonId_idx" ON "Episode"("seasonId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Episode_airDate_idx" ON "Episode"("airDate");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Episode_titleId_seasonNumber_episodeNumber_key" ON "Episode"("titleId", "seasonNumber", "episodeNumber");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WatchProvider_tmdbId_key" ON "WatchProvider"("tmdbId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TitleProvider_titleId_region_idx" ON "TitleProvider"("titleId", "region");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TitleProvider_titleId_providerId_region_kind_key" ON "TitleProvider"("titleId", "providerId", "region", "kind");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LibraryEntry_userId_status_idx" ON "LibraryEntry"("userId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LibraryEntry_userId_updatedAt_idx" ON "LibraryEntry"("userId", "updatedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "LibraryEntry_deletedAt_idx" ON "LibraryEntry"("deletedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "LibraryEntry_userId_titleId_key" ON "LibraryEntry"("userId", "titleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Tag_userId_name_key" ON "Tag"("userId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TagOnEntry_entryId_idx" ON "TagOnEntry"("entryId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Watch_userId_watchedAt_idx" ON "Watch"("userId", "watchedAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Watch_userId_titleId_idx" ON "Watch"("userId", "titleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Watch_entryId_idx" ON "Watch"("entryId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Watch_userId_episodeId_run_key" ON "Watch"("userId", "episodeId", "run");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Universe_ownerId_idx" ON "Universe"("ownerId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Universe_ownerId_slug_key" ON "Universe"("ownerId", "slug");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UniverseMember_userId_idx" ON "UniverseMember"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UniverseMember_universeId_userId_key" ON "UniverseMember"("universeId", "userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UniverseItem_universeId_idx" ON "UniverseItem"("universeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UniverseItem_universeId_titleId_key" ON "UniverseItem"("universeId", "titleId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UniverseOrder_universeId_idx" ON "UniverseOrder"("universeId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "UniverseOrderItem_orderId_position_idx" ON "UniverseOrderItem"("orderId", "position");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UniverseOrderItem_orderId_itemId_key" ON "UniverseOrderItem"("orderId", "itemId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "NotificationChannel_userId_idx" ON "NotificationChannel"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "NotificationLog_userId_sentAt_idx" ON "NotificationLog"("userId", "sentAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "NotificationLog_userId_dedupeKey_key" ON "NotificationLog"("userId", "dedupeKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SyncRun_kind_startedAt_idx" ON "SyncRun"("kind", "startedAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "OidcAccount" ADD CONSTRAINT "OidcAccount_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Season" ADD CONSTRAINT "Season_titleId_fkey" FOREIGN KEY ("titleId") REFERENCES "Title"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Episode" ADD CONSTRAINT "Episode_titleId_fkey" FOREIGN KEY ("titleId") REFERENCES "Title"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Episode" ADD CONSTRAINT "Episode_seasonId_fkey" FOREIGN KEY ("seasonId") REFERENCES "Season"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TitleProvider" ADD CONSTRAINT "TitleProvider_titleId_fkey" FOREIGN KEY ("titleId") REFERENCES "Title"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TitleProvider" ADD CONSTRAINT "TitleProvider_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "WatchProvider"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LibraryEntry" ADD CONSTRAINT "LibraryEntry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "LibraryEntry" ADD CONSTRAINT "LibraryEntry_titleId_fkey" FOREIGN KEY ("titleId") REFERENCES "Title"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Tag" ADD CONSTRAINT "Tag_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TagOnEntry" ADD CONSTRAINT "TagOnEntry_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "TagOnEntry" ADD CONSTRAINT "TagOnEntry_entryId_fkey" FOREIGN KEY ("entryId") REFERENCES "LibraryEntry"("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_entryId_fkey" FOREIGN KEY ("entryId") REFERENCES "LibraryEntry"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Watch" ADD CONSTRAINT "Watch_titleId_fkey" FOREIGN KEY ("titleId") REFERENCES "Title"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Watch" ADD CONSTRAINT "Watch_episodeId_fkey" FOREIGN KEY ("episodeId") REFERENCES "Episode"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Universe" ADD CONSTRAINT "Universe_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UniverseMember" ADD CONSTRAINT "UniverseMember_universeId_fkey" FOREIGN KEY ("universeId") REFERENCES "Universe"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UniverseMember" ADD CONSTRAINT "UniverseMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UniverseItem" ADD CONSTRAINT "UniverseItem_universeId_fkey" FOREIGN KEY ("universeId") REFERENCES "Universe"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UniverseItem" ADD CONSTRAINT "UniverseItem_titleId_fkey" FOREIGN KEY ("titleId") REFERENCES "Title"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UniverseOrder" ADD CONSTRAINT "UniverseOrder_universeId_fkey" FOREIGN KEY ("universeId") REFERENCES "Universe"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UniverseOrderItem" ADD CONSTRAINT "UniverseOrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "UniverseOrder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "UniverseOrderItem" ADD CONSTRAINT "UniverseOrderItem_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "UniverseItem"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "NotificationChannel" ADD CONSTRAINT "NotificationChannel_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "NotificationLog" ADD CONSTRAINT "NotificationLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,510 @@
|
||||
// Flimmer — Datenmodell
|
||||
//
|
||||
// Aufbau in vier Schichten:
|
||||
// 1. Konten & Zugang (User, Session, ApiKey, OidcAccount, AppSetting)
|
||||
// 2. Katalog (Title, Season, Episode, WatchProvider …)
|
||||
// — geteilt zwischen allen Nutzern, dedupliziert über die TMDB-ID
|
||||
// 3. Persönliches Tracking (LibraryEntry, Watch, Tag …)
|
||||
// 4. Universen (Universe, UniverseItem, UniverseOrder …)
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
runtime = "nodejs"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 1. Konten & Zugang
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum Role {
|
||||
ADMIN
|
||||
USER
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String
|
||||
passwordHash String?
|
||||
role Role @default(USER)
|
||||
isActive Boolean @default(true)
|
||||
|
||||
/// Region für die Streaming-Verfügbarkeit (ISO-3166-1, z. B. "DE")
|
||||
region String @default("DE")
|
||||
locale String @default("de")
|
||||
|
||||
/// Öffentliche Profilseite unter /u/<profileSlug>
|
||||
publicProfile Boolean @default(false)
|
||||
profileSlug String? @unique
|
||||
|
||||
// Benachrichtigungswünsche
|
||||
notifyNewEpisode Boolean @default(true)
|
||||
notifyRelease Boolean @default(true)
|
||||
notifyAvailability Boolean @default(false)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
sessions Session[]
|
||||
apiKeys ApiKey[]
|
||||
oidcAccounts OidcAccount[]
|
||||
entries LibraryEntry[]
|
||||
watches Watch[]
|
||||
tags Tag[]
|
||||
universes Universe[] @relation("UniverseOwner")
|
||||
memberships UniverseMember[]
|
||||
channels NotificationChannel[]
|
||||
notifications NotificationLog[]
|
||||
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
|
||||
/// SHA-256 des Cookie-Tokens — das Klartext-Token verlässt den Server nie erneut
|
||||
tokenHash String @unique
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
expiresAt DateTime
|
||||
userAgent String?
|
||||
ip String?
|
||||
createdAt DateTime @default(now())
|
||||
lastSeen DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
@@index([expiresAt])
|
||||
}
|
||||
|
||||
model ApiKey {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
/// Sichtbares Präfix zur Wiedererkennung, z. B. "flm_a1b2c3d4"
|
||||
prefix String @unique
|
||||
keyHash String @unique
|
||||
|
||||
lastUsedAt DateTime?
|
||||
revokedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model OidcAccount {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
issuer String
|
||||
subject String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([issuer, subject])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
/// Instanzweite Einstellungen (Registrierung offen/zu, Instanzname …)
|
||||
model AppSetting {
|
||||
key String @id
|
||||
value Json
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 2. Katalog
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum TitleType {
|
||||
SERIES
|
||||
MOVIE
|
||||
}
|
||||
|
||||
model Title {
|
||||
id String @id @default(cuid())
|
||||
type TitleType
|
||||
|
||||
/// TMDB-ID; null bei manuell angelegten Titeln ohne TMDB-Treffer
|
||||
tmdbId Int?
|
||||
imdbId String?
|
||||
|
||||
title String
|
||||
originalTitle String?
|
||||
overview String?
|
||||
tagline String?
|
||||
posterPath String?
|
||||
backdropPath String?
|
||||
|
||||
/// Erstausstrahlung (Serie) bzw. Kinostart (Film)
|
||||
releaseDate DateTime?
|
||||
lastAirDate DateTime?
|
||||
|
||||
/// Laufzeit in Minuten: bei Filmen die Filmlänge, bei Serien die
|
||||
/// durchschnittliche Episodenlänge (Rückfallwert für Episoden ohne Angabe)
|
||||
runtime Int?
|
||||
|
||||
/// TMDB-Status: "Returning Series", "Ended", "Released", "Planned" …
|
||||
status String?
|
||||
inProduction Boolean @default(false)
|
||||
|
||||
genres String[]
|
||||
originalLang String?
|
||||
homepage String?
|
||||
voteAverage Float?
|
||||
|
||||
seasonCount Int @default(0)
|
||||
episodeCount Int @default(0)
|
||||
|
||||
/// Offizielle TMDB-Collection (z. B. „Spider-Man Collection")
|
||||
tmdbCollectionId Int?
|
||||
tmdbCollectionName String?
|
||||
|
||||
/// Zeitpunkt des letzten TMDB-Abgleichs
|
||||
syncedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
seasons Season[]
|
||||
episodes Episode[]
|
||||
entries LibraryEntry[]
|
||||
watches Watch[]
|
||||
providers TitleProvider[]
|
||||
universeItems UniverseItem[]
|
||||
|
||||
@@unique([type, tmdbId])
|
||||
@@index([type])
|
||||
@@index([releaseDate])
|
||||
@@index([title])
|
||||
@@index([tmdbCollectionId])
|
||||
}
|
||||
|
||||
model Season {
|
||||
id String @id @default(cuid())
|
||||
titleId String
|
||||
title Title @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||
|
||||
tmdbId Int?
|
||||
seasonNumber Int
|
||||
name String
|
||||
overview String?
|
||||
posterPath String?
|
||||
airDate DateTime?
|
||||
episodeCount Int @default(0)
|
||||
|
||||
episodes Episode[]
|
||||
|
||||
@@unique([titleId, seasonNumber])
|
||||
@@index([titleId])
|
||||
}
|
||||
|
||||
model Episode {
|
||||
id String @id @default(cuid())
|
||||
titleId String
|
||||
title Title @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||
seasonId String
|
||||
season Season @relation(fields: [seasonId], references: [id], onDelete: Cascade)
|
||||
|
||||
tmdbId Int?
|
||||
seasonNumber Int
|
||||
episodeNumber Int
|
||||
name String
|
||||
overview String?
|
||||
stillPath String?
|
||||
airDate DateTime?
|
||||
runtime Int?
|
||||
|
||||
watches Watch[]
|
||||
|
||||
@@unique([titleId, seasonNumber, episodeNumber])
|
||||
@@index([titleId])
|
||||
@@index([seasonId])
|
||||
@@index([airDate])
|
||||
}
|
||||
|
||||
/// Streamingdienst laut TMDB/JustWatch
|
||||
model WatchProvider {
|
||||
id String @id @default(cuid())
|
||||
tmdbId Int @unique
|
||||
name String
|
||||
logoPath String?
|
||||
/// Akzentfarbe für Badges im UI (Hex); fällt sonst auf einen Standardton zurück
|
||||
color String?
|
||||
|
||||
titles TitleProvider[]
|
||||
}
|
||||
|
||||
enum ProviderKind {
|
||||
FLATRATE
|
||||
RENT
|
||||
BUY
|
||||
ADS
|
||||
FREE
|
||||
}
|
||||
|
||||
model TitleProvider {
|
||||
id String @id @default(cuid())
|
||||
titleId String
|
||||
title Title @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||
providerId String
|
||||
provider WatchProvider @relation(fields: [providerId], references: [id], onDelete: Cascade)
|
||||
region String
|
||||
kind ProviderKind
|
||||
/// Für die Benachrichtigung „ist jetzt verfügbar"
|
||||
firstSeenAt DateTime @default(now())
|
||||
|
||||
@@unique([titleId, providerId, region, kind])
|
||||
@@index([titleId, region])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 3. Persönliches Tracking
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum EntryStatus {
|
||||
WATCHING
|
||||
PAUSED
|
||||
COMPLETED
|
||||
DROPPED
|
||||
WATCHLIST
|
||||
}
|
||||
|
||||
model LibraryEntry {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
titleId String
|
||||
title Title @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||
|
||||
status EntryStatus @default(WATCHLIST)
|
||||
|
||||
/// Eigene Bewertung 1–10
|
||||
rating Int?
|
||||
notes String?
|
||||
|
||||
/// „Wo schaue ich das" — Vorschlag aus der Verfügbarkeit, überschreibbar
|
||||
whereToWatch String?
|
||||
|
||||
favorite Boolean @default(false)
|
||||
|
||||
/// Aktueller Durchlauf: 0 = Erstsichtung, 1 = erster Rewatch …
|
||||
currentRun Int @default(0)
|
||||
|
||||
startedAt DateTime?
|
||||
finishedAt DateTime?
|
||||
|
||||
/// Papierkorb: 30 Tage aufbewahren statt hart löschen
|
||||
deletedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
tags TagOnEntry[]
|
||||
watches Watch[]
|
||||
|
||||
@@unique([userId, titleId])
|
||||
@@index([userId, status])
|
||||
@@index([userId, updatedAt])
|
||||
@@index([deletedAt])
|
||||
}
|
||||
|
||||
model Tag {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
color String?
|
||||
|
||||
entries TagOnEntry[]
|
||||
|
||||
@@unique([userId, name])
|
||||
}
|
||||
|
||||
model TagOnEntry {
|
||||
tagId String
|
||||
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||
entryId String
|
||||
entry LibraryEntry @relation(fields: [entryId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([tagId, entryId])
|
||||
@@index([entryId])
|
||||
}
|
||||
|
||||
/// Ein Sichtungsvorgang: entweder eine Episode oder ein Film.
|
||||
/// Rewatches werden über `run` gezählt (0 = Erstsichtung).
|
||||
model Watch {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
entryId String?
|
||||
entry LibraryEntry? @relation(fields: [entryId], references: [id], onDelete: SetNull)
|
||||
|
||||
titleId String
|
||||
title Title @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||
episodeId String?
|
||||
episode Episode? @relation(fields: [episodeId], references: [id], onDelete: Cascade)
|
||||
|
||||
run Int @default(0)
|
||||
watchedAt DateTime @default(now())
|
||||
|
||||
/// Tatsächlich angerechnete Minuten — friert die Laufzeit zum Zeitpunkt der
|
||||
/// Sichtung ein, damit spätere TMDB-Korrekturen die Statistik nicht verbiegen.
|
||||
minutes Int @default(0)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([userId, episodeId, run])
|
||||
@@index([userId, watchedAt])
|
||||
@@index([userId, titleId])
|
||||
@@index([entryId])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 4. Universen
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
model Universe {
|
||||
id String @id @default(cuid())
|
||||
ownerId String
|
||||
owner User @relation("UniverseOwner", fields: [ownerId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
slug String
|
||||
description String?
|
||||
coverPath String?
|
||||
|
||||
deletedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
items UniverseItem[]
|
||||
orders UniverseOrder[]
|
||||
members UniverseMember[]
|
||||
|
||||
@@unique([ownerId, slug])
|
||||
@@index([ownerId])
|
||||
}
|
||||
|
||||
enum SharePermission {
|
||||
READ
|
||||
WRITE
|
||||
}
|
||||
|
||||
model UniverseMember {
|
||||
id String @id @default(cuid())
|
||||
universeId String
|
||||
universe Universe @relation(fields: [universeId], references: [id], onDelete: Cascade)
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
permission SharePermission @default(READ)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([universeId, userId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model UniverseItem {
|
||||
id String @id @default(cuid())
|
||||
universeId String
|
||||
universe Universe @relation(fields: [universeId], references: [id], onDelete: Cascade)
|
||||
titleId String
|
||||
title Title @relation(fields: [titleId], references: [id], onDelete: Cascade)
|
||||
note String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
positions UniverseOrderItem[]
|
||||
|
||||
@@unique([universeId, titleId])
|
||||
@@index([universeId])
|
||||
}
|
||||
|
||||
enum OrderKind {
|
||||
/// Automatisch aus den Erscheinungsdaten berechnet
|
||||
RELEASE
|
||||
/// Frei sortierbar (Story-Chronologie, Einsteiger-Reihenfolge …)
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
model UniverseOrder {
|
||||
id String @id @default(cuid())
|
||||
universeId String
|
||||
universe Universe @relation(fields: [universeId], references: [id], onDelete: Cascade)
|
||||
name String
|
||||
kind OrderKind @default(CUSTOM)
|
||||
isDefault Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
items UniverseOrderItem[]
|
||||
|
||||
@@index([universeId])
|
||||
}
|
||||
|
||||
model UniverseOrderItem {
|
||||
id String @id @default(cuid())
|
||||
orderId String
|
||||
order UniverseOrder @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||
itemId String
|
||||
item UniverseItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
||||
position Int
|
||||
|
||||
@@unique([orderId, itemId])
|
||||
@@index([orderId, position])
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Benachrichtigungen & Hintergrundläufe
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum ChannelKind {
|
||||
EMAIL
|
||||
NTFY
|
||||
GOTIFY
|
||||
WEBHOOK
|
||||
}
|
||||
|
||||
model NotificationChannel {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
kind ChannelKind
|
||||
name String
|
||||
/// Kanalspezifische Einstellungen (URL, Topic, Token …)
|
||||
config Json
|
||||
enabled Boolean @default(true)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model NotificationLog {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
/// Verhindert Doppelversand, z. B. "episode:<episodeId>"
|
||||
dedupeKey String
|
||||
subject String
|
||||
body String
|
||||
sentAt DateTime @default(now())
|
||||
ok Boolean @default(true)
|
||||
error String?
|
||||
|
||||
@@unique([userId, dedupeKey])
|
||||
@@index([userId, sentAt])
|
||||
}
|
||||
|
||||
model SyncRun {
|
||||
id String @id @default(cuid())
|
||||
kind String
|
||||
startedAt DateTime @default(now())
|
||||
finishedAt DateTime?
|
||||
ok Boolean @default(false)
|
||||
message String?
|
||||
stats Json?
|
||||
|
||||
@@index([kind, startedAt])
|
||||
}
|
||||
Reference in New Issue
Block a user