Initial commit: YouTube Studio Flow (backend, frontend, infrastructure, docs)

This commit is contained in:
2026-08-11 12:27:44 +02:00
commit d5af006443
304 changed files with 74604 additions and 0 deletions
+27
View File
@@ -0,0 +1,27 @@
/**
* Usage: npx ts-node prisma/make-admin.ts <email>
* Promotes an existing user to app-level admin (isAppAdmin = true) by email.
* Run this once after the first Google sign-in.
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const email = process.argv[2];
if (!email) {
console.error('Usage: npx ts-node prisma/make-admin.ts <email>');
process.exit(1);
}
const user = await prisma.user.update({
where: { email },
data: { isAppAdmin: true },
});
console.log(`${user.name} (${user.email}) is now an app admin`);
}
main()
.catch((e) => { console.error(e); process.exit(1); })
.finally(() => prisma.$disconnect());
@@ -0,0 +1,347 @@
-- CreateEnum
CREATE TYPE "PrivacyStatus" AS ENUM ('PUBLIC', 'PRIVATE', 'UNLISTED');
-- CreateEnum
CREATE TYPE "LintStatus" AS ENUM ('OK', 'WARNING', 'ERROR');
-- CreateEnum
CREATE TYPE "BlockType" AS ENUM ('STATIC', 'VARIABLE', 'CONDITIONAL', 'REPEATABLE', 'GLOBAL', 'CAMPAIGN', 'COLLABORATOR');
-- CreateEnum
CREATE TYPE "LintSeverity" AS ENUM ('INFO', 'WARNING', 'ERROR');
-- CreateEnum
CREATE TYPE "BulkJobStatus" AS ENUM ('PENDING', 'DRY_RUN', 'CONFIRMED', 'RUNNING', 'DONE', 'FAILED', 'ROLLED_BACK');
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('ADMIN', 'EDITOR', 'REVIEWER', 'READONLY');
-- CreateTable
CREATE TABLE "Video" (
"id" TEXT NOT NULL,
"youtubeVideoId" TEXT NOT NULL,
"channelId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"renderedDescription" TEXT,
"tags" TEXT[],
"categoryId" TEXT,
"privacyStatus" "PrivacyStatus" NOT NULL DEFAULT 'PRIVATE',
"publishedAt" TIMESTAMP(3),
"scheduledAt" TIMESTAMP(3),
"templateId" TEXT,
"lintStatus" "LintStatus" NOT NULL DEFAULT 'OK',
"lastSyncedAt" TIMESTAMP(3),
"lastSyncedHash" TEXT,
"remoteConflict" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Video_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VideoConfig" (
"id" TEXT NOT NULL,
"videoId" TEXT NOT NULL,
"templateId" TEXT,
"blockOrder" JSONB NOT NULL,
"blockOverrides" JSONB NOT NULL,
"variableValues" JSONB NOT NULL,
"collaboratorIds" JSONB NOT NULL,
"version" INTEGER NOT NULL DEFAULT 1,
"renderHash" TEXT,
"renderedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "VideoConfig_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "DescriptionBlock" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"type" "BlockType" NOT NULL,
"content" TEXT NOT NULL,
"language" TEXT NOT NULL DEFAULT 'de',
"campaignId" TEXT,
"version" INTEGER NOT NULL DEFAULT 1,
"active" BOOLEAN NOT NULL DEFAULT true,
"tags" TEXT[],
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "DescriptionBlock_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BlockVersion" (
"id" TEXT NOT NULL,
"blockId" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"contentSnapshot" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdBy" TEXT,
CONSTRAINT "BlockVersion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Template" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"defaultBlocks" JSONB NOT NULL,
"rules" JSONB NOT NULL,
"variables" JSONB NOT NULL,
"version" INTEGER NOT NULL DEFAULT 1,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Template_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TemplateVersion" (
"id" TEXT NOT NULL,
"templateId" TEXT NOT NULL,
"version" INTEGER NOT NULL,
"snapshot" JSONB NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "TemplateVersion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Collaborator" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"youtubeHandle" TEXT NOT NULL,
"twitchLink" TEXT,
"aliases" TEXT[],
"active" BOOLEAN NOT NULL DEFAULT true,
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Collaborator_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VideoCollaborator" (
"videoId" TEXT NOT NULL,
"collaboratorId" TEXT NOT NULL,
"role" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "VideoCollaborator_pkey" PRIMARY KEY ("videoId","collaboratorId")
);
-- CreateTable
CREATE TABLE "SavedView" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"isGlobal" BOOLEAN NOT NULL DEFAULT false,
"ownerId" TEXT,
"queryJson" JSONB NOT NULL,
"columnsJson" JSONB NOT NULL,
"sortJson" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SavedView_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "LintResult" (
"id" TEXT NOT NULL,
"videoId" TEXT NOT NULL,
"ruleCode" TEXT NOT NULL,
"severity" "LintSeverity" NOT NULL,
"targetField" TEXT,
"message" TEXT NOT NULL,
"fixSuggestion" TEXT,
"resolvedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "LintResult_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BulkJob" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"initiatedBy" TEXT NOT NULL,
"filterSnapshot" JSONB NOT NULL,
"targetIds" TEXT[],
"status" "BulkJobStatus" NOT NULL DEFAULT 'PENDING',
"dryRunResult" JSONB,
"rollbackData" JSONB,
"totalCount" INTEGER NOT NULL,
"successCount" INTEGER NOT NULL DEFAULT 0,
"errorCount" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" TIMESTAMP(3),
CONSTRAINT "BulkJob_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BulkJobItem" (
"id" TEXT NOT NULL,
"bulkJobId" TEXT NOT NULL,
"videoId" TEXT NOT NULL,
"beforeSnapshot" JSONB,
"afterSnapshot" JSONB,
"status" TEXT NOT NULL DEFAULT 'pending',
"errorMessage" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "BulkJobItem_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Campaign" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"startAt" TIMESTAMP(3) NOT NULL,
"endAt" TIMESTAMP(3),
"status" TEXT NOT NULL DEFAULT 'active',
"notes" TEXT,
CONSTRAINT "Campaign_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ImportJob" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"sourceName" TEXT NOT NULL,
"mappingJson" JSONB,
"validationReport" JSONB,
"commitStatus" TEXT NOT NULL DEFAULT 'pending',
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"committedAt" TIMESTAMP(3),
CONSTRAINT "ImportJob_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ExportJob" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"scopeJson" JSONB NOT NULL,
"fileReference" TEXT,
"createdBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ExportJob_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "QuotaLog" (
"id" TEXT NOT NULL,
"datePt" TIMESTAMP(3) NOT NULL,
"units" INTEGER NOT NULL,
"operation" TEXT NOT NULL,
"entityId" TEXT,
"videoId" TEXT,
"bulkJobId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "QuotaLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AuditLog" (
"id" TEXT NOT NULL,
"actorId" TEXT NOT NULL,
"entityType" TEXT NOT NULL,
"entityId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"beforeJson" JSONB,
"afterJson" JSONB,
"requestId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
"googleId" TEXT NOT NULL,
"role" "UserRole" NOT NULL DEFAULT 'EDITOR',
"youtubeAccessToken" TEXT,
"youtubeRefreshToken" TEXT,
"youtubeTokenExpiry" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Video_youtubeVideoId_key" ON "Video"("youtubeVideoId");
-- CreateIndex
CREATE UNIQUE INDEX "VideoConfig_videoId_key" ON "VideoConfig"("videoId");
-- CreateIndex
CREATE INDEX "LintResult_videoId_severity_idx" ON "LintResult"("videoId", "severity");
-- CreateIndex
CREATE INDEX "LintResult_ruleCode_idx" ON "LintResult"("ruleCode");
-- CreateIndex
CREATE INDEX "QuotaLog_datePt_idx" ON "QuotaLog"("datePt");
-- CreateIndex
CREATE INDEX "AuditLog_entityType_entityId_idx" ON "AuditLog"("entityType", "entityId");
-- CreateIndex
CREATE INDEX "AuditLog_createdAt_idx" ON "AuditLog"("createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "User_googleId_key" ON "User"("googleId");
-- AddForeignKey
ALTER TABLE "Video" ADD CONSTRAINT "Video_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "VideoConfig" ADD CONSTRAINT "VideoConfig_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DescriptionBlock" ADD CONSTRAINT "DescriptionBlock_campaignId_fkey" FOREIGN KEY ("campaignId") REFERENCES "Campaign"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BlockVersion" ADD CONSTRAINT "BlockVersion_blockId_fkey" FOREIGN KEY ("blockId") REFERENCES "DescriptionBlock"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TemplateVersion" ADD CONSTRAINT "TemplateVersion_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "VideoCollaborator" ADD CONSTRAINT "VideoCollaborator_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "VideoCollaborator" ADD CONSTRAINT "VideoCollaborator_collaboratorId_fkey" FOREIGN KEY ("collaboratorId") REFERENCES "Collaborator"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "LintResult" ADD CONSTRAINT "LintResult_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BulkJobItem" ADD CONSTRAINT "BulkJobItem_bulkJobId_fkey" FOREIGN KEY ("bulkJobId") REFERENCES "BulkJob"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BulkJobItem" ADD CONSTRAINT "BulkJobItem_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "QuotaLog" ADD CONSTRAINT "QuotaLog_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,175 @@
/*
Warnings:
- You are about to drop the column `role` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `youtubeAccessToken` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `youtubeRefreshToken` on the `User` table. All the data in the column will be lost.
- You are about to drop the column `youtubeTokenExpiry` on the `User` table. All the data in the column will be lost.
- Added the required column `teamId` to the `BulkJob` table without a default value. This is not possible if the table is not empty.
- Added the required column `teamId` to the `Campaign` table without a default value. This is not possible if the table is not empty.
- Added the required column `teamId` to the `Collaborator` table without a default value. This is not possible if the table is not empty.
- Added the required column `teamId` to the `DescriptionBlock` table without a default value. This is not possible if the table is not empty.
- Added the required column `teamId` to the `ExportJob` table without a default value. This is not possible if the table is not empty.
- Added the required column `teamId` to the `ImportJob` table without a default value. This is not possible if the table is not empty.
- Added the required column `teamId` to the `SavedView` table without a default value. This is not possible if the table is not empty.
- Added the required column `teamId` to the `Template` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "TeamRole" AS ENUM ('OWNER', 'ADMIN', 'EDITOR', 'REVIEWER', 'READONLY');
-- AlterTable
ALTER TABLE "BulkJob" ADD COLUMN "teamId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "Campaign" ADD COLUMN "teamId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "Collaborator" ADD COLUMN "teamId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "DescriptionBlock" ADD COLUMN "teamId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "ExportJob" ADD COLUMN "teamId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "ImportJob" ADD COLUMN "teamId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "QuotaLog" ADD COLUMN "channelId" TEXT;
-- AlterTable
ALTER TABLE "SavedView" ADD COLUMN "teamId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "Template" ADD COLUMN "teamId" TEXT NOT NULL;
-- AlterTable
ALTER TABLE "User" DROP COLUMN "role",
DROP COLUMN "youtubeAccessToken",
DROP COLUMN "youtubeRefreshToken",
DROP COLUMN "youtubeTokenExpiry",
ADD COLUMN "isAppAdmin" BOOLEAN NOT NULL DEFAULT false;
-- DropEnum
DROP TYPE "UserRole";
-- CreateTable
CREATE TABLE "Team" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Team_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "TeamMember" (
"userId" TEXT NOT NULL,
"teamId" TEXT NOT NULL,
"role" "TeamRole" NOT NULL DEFAULT 'EDITOR',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "TeamMember_pkey" PRIMARY KEY ("userId","teamId")
);
-- CreateTable
CREATE TABLE "Channel" (
"id" TEXT NOT NULL,
"teamId" TEXT NOT NULL,
"youtubeChannelId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"uploadsPlaylistId" TEXT,
"youtubeAccessToken" TEXT,
"youtubeRefreshToken" TEXT,
"youtubeTokenExpiry" TIMESTAMP(3),
"connectedBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Channel_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Team_slug_key" ON "Team"("slug");
-- CreateIndex
CREATE INDEX "TeamMember_teamId_idx" ON "TeamMember"("teamId");
-- CreateIndex
CREATE UNIQUE INDEX "Channel_youtubeChannelId_key" ON "Channel"("youtubeChannelId");
-- CreateIndex
CREATE INDEX "Channel_teamId_idx" ON "Channel"("teamId");
-- CreateIndex
CREATE INDEX "BulkJob_teamId_idx" ON "BulkJob"("teamId");
-- CreateIndex
CREATE INDEX "Campaign_teamId_idx" ON "Campaign"("teamId");
-- CreateIndex
CREATE INDEX "Collaborator_teamId_idx" ON "Collaborator"("teamId");
-- CreateIndex
CREATE INDEX "DescriptionBlock_teamId_idx" ON "DescriptionBlock"("teamId");
-- CreateIndex
CREATE INDEX "ExportJob_teamId_idx" ON "ExportJob"("teamId");
-- CreateIndex
CREATE INDEX "ImportJob_teamId_idx" ON "ImportJob"("teamId");
-- CreateIndex
CREATE INDEX "QuotaLog_channelId_idx" ON "QuotaLog"("channelId");
-- CreateIndex
CREATE INDEX "SavedView_teamId_idx" ON "SavedView"("teamId");
-- CreateIndex
CREATE INDEX "Template_teamId_idx" ON "Template"("teamId");
-- CreateIndex
CREATE INDEX "Video_channelId_idx" ON "Video"("channelId");
-- AddForeignKey
ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Channel" ADD CONSTRAINT "Channel_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Video" ADD CONSTRAINT "Video_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "DescriptionBlock" ADD CONSTRAINT "DescriptionBlock_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Template" ADD CONSTRAINT "Template_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Collaborator" ADD CONSTRAINT "Collaborator_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SavedView" ADD CONSTRAINT "SavedView_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BulkJob" ADD CONSTRAINT "BulkJob_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Campaign" ADD CONSTRAINT "Campaign_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ImportJob" ADD CONSTRAINT "ImportJob_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ExportJob" ADD CONSTRAINT "ExportJob_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "QuotaLog" ADD CONSTRAINT "QuotaLog_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,23 @@
-- AlterTable
ALTER TABLE "DescriptionBlock" ADD COLUMN "variableDefinitions" JSONB NOT NULL DEFAULT '[]';
-- CreateTable
CREATE TABLE "TeamVariable" (
"id" TEXT NOT NULL,
"teamId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"value" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "TeamVariable_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "TeamVariable_teamId_idx" ON "TeamVariable"("teamId");
-- CreateIndex
CREATE UNIQUE INDEX "TeamVariable_teamId_name_key" ON "TeamVariable"("teamId", "name");
-- AddForeignKey
ALTER TABLE "TeamVariable" ADD CONSTRAINT "TeamVariable_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Video" ADD COLUMN "youtubeDescription" TEXT;
@@ -0,0 +1,50 @@
-- AlterTable
ALTER TABLE "Video" ADD COLUMN "ageRestricted" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "containsPaidPromotion" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "defaultAudioLanguage" TEXT,
ADD COLUMN "defaultLanguage" TEXT,
ADD COLUMN "embeddable" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "gameTitle" TEXT,
ADD COLUMN "license" TEXT NOT NULL DEFAULT 'youtube',
ADD COLUMN "madeForKids" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "recordingDate" TIMESTAMP(3),
ADD COLUMN "selfDeclaredMadeForKids" BOOLEAN NOT NULL DEFAULT false;
-- CreateTable
CREATE TABLE "Playlist" (
"id" TEXT NOT NULL,
"channelId" TEXT NOT NULL,
"youtubePlaylistId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"itemCount" INTEGER NOT NULL DEFAULT 0,
"privacyStatus" TEXT NOT NULL DEFAULT 'public',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Playlist_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VideoPlaylist" (
"videoId" TEXT NOT NULL,
"playlistId" TEXT NOT NULL,
"position" INTEGER,
CONSTRAINT "VideoPlaylist_pkey" PRIMARY KEY ("videoId","playlistId")
);
-- CreateIndex
CREATE UNIQUE INDEX "Playlist_youtubePlaylistId_key" ON "Playlist"("youtubePlaylistId");
-- CreateIndex
CREATE INDEX "Playlist_channelId_idx" ON "Playlist"("channelId");
-- AddForeignKey
ALTER TABLE "Playlist" ADD CONSTRAINT "Playlist_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "VideoPlaylist" ADD CONSTRAINT "VideoPlaylist_videoId_fkey" FOREIGN KEY ("videoId") REFERENCES "Video"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "VideoPlaylist" ADD CONSTRAINT "VideoPlaylist_playlistId_fkey" FOREIGN KEY ("playlistId") REFERENCES "Playlist"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "preferences" JSONB;
@@ -0,0 +1,2 @@
-- Make youtubeHandle optional on Collaborator, consistent with all other platform fields
ALTER TABLE "Collaborator" ALTER COLUMN "youtubeHandle" DROP NOT NULL;
@@ -0,0 +1,9 @@
-- Rename youtubeHandle -> youtubeLink for consistency with other platform fields
ALTER TABLE "Collaborator" RENAME COLUMN "youtubeHandle" TO "youtubeLink";
-- Convert existing handle values (@handle or handle) to full YouTube URLs
UPDATE "Collaborator"
SET "youtubeLink" = 'https://www.youtube.com/@' || LTRIM("youtubeLink", '@')
WHERE "youtubeLink" IS NOT NULL
AND "youtubeLink" != ''
AND "youtubeLink" NOT LIKE 'https://%';
@@ -0,0 +1 @@
ALTER TABLE "DescriptionBlock" ADD COLUMN "compact" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1 @@
ALTER TABLE "Template" ADD COLUMN "defaultOverrides" JSONB NOT NULL DEFAULT '{}'::jsonb;
@@ -0,0 +1,17 @@
-- Add collaboratorIds directly to Video
ALTER TABLE "Video" ADD COLUMN "collaboratorIds" JSONB NOT NULL DEFAULT '[]';
-- Migrate existing data from VideoConfig.collaboratorIds into Video.collaboratorIds
UPDATE "Video" v
SET "collaboratorIds" = vc."collaboratorIds"
FROM "VideoConfig" vc
WHERE vc."videoId" = v.id
AND vc."collaboratorIds" IS NOT NULL
AND vc."collaboratorIds"::text != '[]'
AND vc."collaboratorIds"::text != 'null';
-- Drop VideoCollaborator join table
DROP TABLE "VideoCollaborator";
-- Remove collaboratorIds from VideoConfig
ALTER TABLE "VideoConfig" DROP COLUMN "collaboratorIds";
@@ -0,0 +1 @@
ALTER TABLE "Video" ADD COLUMN "thumbnailUrl" TEXT;
@@ -0,0 +1,2 @@
-- Add YouTube baseline snapshot to Video for full-field diff
ALTER TABLE "Video" ADD COLUMN "youtubeSnapshot" JSONB;
@@ -0,0 +1,3 @@
ALTER TABLE "QuotaLog" ADD COLUMN "actionId" TEXT;
ALTER TABLE "QuotaLog" ADD COLUMN "actionType" TEXT;
CREATE INDEX "QuotaLog_actionId_idx" ON "QuotaLog"("actionId");
@@ -0,0 +1,2 @@
ALTER TABLE "Team" ADD COLUMN "timezone" TEXT NOT NULL DEFAULT 'UTC';
ALTER TABLE "Team" ADD COLUMN "publishingSchedule" JSONB;
@@ -0,0 +1 @@
ALTER TABLE "Team" ADD COLUMN "showCanvaLink" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1 @@
ALTER TABLE "Team" ADD COLUMN "disabledLintRules" TEXT[] NOT NULL DEFAULT '{}';
@@ -0,0 +1 @@
ALTER TABLE "Video" ADD COLUMN "youtubeDeletedAt" TIMESTAMP(3);
@@ -0,0 +1 @@
ALTER TABLE "Team" ADD COLUMN "showDeletedVideos" BOOLEAN NOT NULL DEFAULT false;
@@ -0,0 +1,15 @@
-- Capture schema drift: columns added directly to DB without migration files
ALTER TABLE "Team" ADD COLUMN IF NOT EXISTS "dateFormat" TEXT;
ALTER TABLE "Channel" ADD COLUMN IF NOT EXISTS "supplementalVideoIds" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
ALTER TABLE "DescriptionBlock" ADD COLUMN IF NOT EXISTS "condition" JSONB;
ALTER TABLE "Template" ADD COLUMN IF NOT EXISTS "videoFields" JSONB;
ALTER TABLE "Collaborator" ADD COLUMN IF NOT EXISTS "instagramLink" TEXT;
ALTER TABLE "Collaborator" ADD COLUMN IF NOT EXISTS "tiktokLink" TEXT;
ALTER TABLE "Collaborator" ADD COLUMN IF NOT EXISTS "twitterLink" TEXT;
ALTER TABLE "Collaborator" ADD COLUMN IF NOT EXISTS "blueskyLink" TEXT;
ALTER TABLE "Collaborator" ADD COLUMN IF NOT EXISTS "discordHandle" TEXT;
@@ -0,0 +1,2 @@
-- Fix: remove the default from supplementalVideoIds — column was added without a default in DB
ALTER TABLE "Channel" ALTER COLUMN "supplementalVideoIds" DROP DEFAULT;
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "SavedView" ADD COLUMN "pinnedAsTab" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "tabOrder" INTEGER;
@@ -0,0 +1,8 @@
ALTER TABLE "Team"
ADD COLUMN "conflictDetectionEnabled" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "conflictDetectionBatchSize" INTEGER NOT NULL DEFAULT 50,
ADD COLUMN "conflictDetectionMinAgeDays" INTEGER NOT NULL DEFAULT 7;
ALTER TABLE "Video"
ADD COLUMN "pendingRemoteSnapshot" JSONB,
ADD COLUMN "pendingRemoteDescription" TEXT;
@@ -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"
+492
View File
@@ -0,0 +1,492 @@
generator client {
provider = "prisma-client-js"
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ─── USERS ────────────────────────────────────────────────
model User {
id String @id @default(cuid())
email String @unique
name String?
googleId String @unique
isAppAdmin Boolean @default(false)
preferences Json?
teams TeamMember[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ─── TEAMS ────────────────────────────────────────────────
model Team {
id String @id @default(cuid())
name String
slug String @unique
dateFormat String?
timezone String @default("UTC")
publishingSchedule Json?
showCanvaLink Boolean @default(false)
disabledLintRules String[] @default([])
showDeletedVideos Boolean @default(false)
conflictDetectionEnabled Boolean @default(false)
conflictDetectionBatchSize Int @default(50)
conflictDetectionMinAgeDays Int @default(7)
members TeamMember[]
channels Channel[]
blocks DescriptionBlock[]
templates Template[]
collaborators Collaborator[]
bulkJobs BulkJob[]
savedViews SavedView[]
importJobs ImportJob[]
exportJobs ExportJob[]
campaigns Campaign[]
variables TeamVariable[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model TeamVariable {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id])
name String
value String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([teamId, name])
@@index([teamId])
}
model TeamMember {
userId String
teamId String
role TeamRole @default(EDITOR)
user User @relation(fields: [userId], references: [id])
team Team @relation(fields: [teamId], references: [id])
createdAt DateTime @default(now())
@@id([userId, teamId])
@@index([teamId])
}
enum TeamRole {
OWNER
ADMIN
EDITOR
REVIEWER
READONLY
}
// ─── CHANNELS ─────────────────────────────────────────────
model Channel {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id])
youtubeChannelId String @unique
name String
uploadsPlaylistId String?
supplementalVideoIds String[]
youtubeAccessToken String?
youtubeRefreshToken String?
youtubeTokenExpiry DateTime?
connectedBy String
videos Video[]
playlists Playlist[]
quotaLogs QuotaLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([teamId])
}
// ─── VIDEOS ───────────────────────────────────────────────
model Video {
id String @id @default(cuid())
youtubeVideoId String @unique
channelId String
channel Channel @relation(fields: [channelId], references: [id])
title String
youtubeDescription String?
renderedDescription String?
thumbnailUrl String?
tags String[]
categoryId String?
privacyStatus PrivacyStatus @default(PRIVATE)
publishedAt DateTime?
scheduledAt DateTime?
templateId String?
// Extended metadata
madeForKids Boolean @default(false)
selfDeclaredMadeForKids Boolean @default(false)
containsPaidPromotion Boolean @default(false)
ageRestricted Boolean @default(false)
embeddable Boolean @default(true)
license String @default("youtube")
defaultLanguage String?
defaultAudioLanguage String?
recordingDate DateTime?
gameTitle String?
collaboratorIds Json @default("[]")
youtubeSnapshot Json?
lintStatus LintStatus @default(OK)
lastSyncedAt DateTime?
lastSyncedHash String?
remoteConflict Boolean @default(false)
pendingRemoteSnapshot Json?
pendingRemoteDescription String?
youtubeDeletedAt DateTime?
template Template? @relation(fields: [templateId], references: [id])
config VideoConfig?
playlists VideoPlaylist[]
lintResults LintResult[]
bulkJobItems BulkJobItem[]
quotaLogs QuotaLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([channelId])
}
enum PrivacyStatus {
PUBLIC
PRIVATE
UNLISTED
}
enum LintStatus {
OK
WARNING
ERROR
}
// ─── VIDEO CONFIG ─────────────────────────────────────────
model VideoConfig {
id String @id @default(cuid())
videoId String @unique
video Video @relation(fields: [videoId], references: [id])
templateId String?
blockOrder Json
blockOverrides Json
variableValues Json
version Int @default(1)
renderHash String?
renderedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// ─── DESCRIPTION BLOCKS ───────────────────────────────────
model DescriptionBlock {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id])
name String
type BlockType
content String
language String @default("de")
campaignId String?
campaign Campaign? @relation(fields: [campaignId], references: [id])
version Int @default(1)
active Boolean @default(true)
compact Boolean @default(false)
tags String[]
variableDefinitions Json @default("[]")
condition Json?
versions BlockVersion[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([teamId])
}
enum BlockType {
STATIC
VARIABLE
CONDITIONAL
REPEATABLE
GLOBAL
CAMPAIGN
COLLABORATOR
}
model BlockVersion {
id String @id @default(cuid())
blockId String
block DescriptionBlock @relation(fields: [blockId], references: [id])
version Int
contentSnapshot Json
createdAt DateTime @default(now())
createdBy String?
}
// ─── TEMPLATES ────────────────────────────────────────────
model Template {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id])
name String
description String?
defaultBlocks Json
defaultOverrides Json @default("{}")
rules Json
variables Json
videoFields Json?
version Int @default(1)
active Boolean @default(true)
videos Video[]
versions TemplateVersion[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([teamId])
}
model TemplateVersion {
id String @id @default(cuid())
templateId String
template Template @relation(fields: [templateId], references: [id])
version Int
snapshot Json
createdAt DateTime @default(now())
}
// ─── COLLABORATORS ────────────────────────────────────────
model Collaborator {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id])
name String
youtubeLink String?
twitchLink String?
instagramLink String?
tiktokLink String?
twitterLink String?
blueskyLink String?
discordHandle String?
aliases String[]
active Boolean @default(true)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([teamId])
}
// ─── SAVED VIEWS ──────────────────────────────────────────
model SavedView {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id])
name String
description String?
isGlobal Boolean @default(false)
ownerId String?
queryJson Json
columnsJson Json
sortJson Json?
pinnedAsTab Boolean @default(false)
tabOrder Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([teamId])
}
// ─── LINT RESULTS ─────────────────────────────────────────
model LintResult {
id String @id @default(cuid())
videoId String
video Video @relation(fields: [videoId], references: [id])
ruleCode String
severity LintSeverity
targetField String?
message String
fixSuggestion String?
resolvedAt DateTime?
createdAt DateTime @default(now())
@@index([videoId, severity])
@@index([ruleCode])
}
enum LintSeverity {
INFO
WARNING
ERROR
}
// ─── BULK JOBS ────────────────────────────────────────────
model BulkJob {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id])
type String
initiatedBy String
filterSnapshot Json
targetIds String[]
status BulkJobStatus @default(PENDING)
dryRunResult Json?
rollbackData Json?
totalCount Int
successCount Int @default(0)
errorCount Int @default(0)
items BulkJobItem[]
createdAt DateTime @default(now())
completedAt DateTime?
@@index([teamId])
}
enum BulkJobStatus {
PENDING
DRY_RUN
CONFIRMED
RUNNING
DONE
FAILED
ROLLED_BACK
}
model BulkJobItem {
id String @id @default(cuid())
bulkJobId String
bulkJob BulkJob @relation(fields: [bulkJobId], references: [id])
videoId String
video Video @relation(fields: [videoId], references: [id])
beforeSnapshot Json?
afterSnapshot Json?
status String @default("pending")
errorMessage String?
createdAt DateTime @default(now())
}
// ─── CAMPAIGNS ────────────────────────────────────────────
model Campaign {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id])
name String
startAt DateTime
endAt DateTime?
status String @default("active")
notes String?
blocks DescriptionBlock[]
@@index([teamId])
}
// ─── PLAYLISTS ────────────────────────────────────────────
model Playlist {
id String @id @default(cuid())
channelId String
channel Channel @relation(fields: [channelId], references: [id])
youtubePlaylistId String @unique
title String
description String?
itemCount Int @default(0)
privacyStatus String @default("public")
videos VideoPlaylist[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([channelId])
}
model VideoPlaylist {
videoId String
playlistId String
position Int?
video Video @relation(fields: [videoId], references: [id])
playlist Playlist @relation(fields: [playlistId], references: [id])
@@id([videoId, playlistId])
}
// ─── IMPORT / EXPORT JOBS ─────────────────────────────────
model ImportJob {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id])
type String
sourceName String
mappingJson Json?
validationReport Json?
commitStatus String @default("pending")
createdBy String
createdAt DateTime @default(now())
committedAt DateTime?
@@index([teamId])
}
model ExportJob {
id String @id @default(cuid())
teamId String
team Team @relation(fields: [teamId], references: [id])
type String
scopeJson Json
fileReference String?
createdBy String
createdAt DateTime @default(now())
@@index([teamId])
}
// ─── YOUTUBE QUOTA LOG ────────────────────────────────────
model QuotaLog {
id String @id @default(cuid())
datePt DateTime
units Int
operation String
entityId String?
channelId String?
channel Channel? @relation(fields: [channelId], references: [id])
videoId String?
video Video? @relation(fields: [videoId], references: [id])
bulkJobId String?
actionId String?
actionType String?
createdAt DateTime @default(now())
@@index([datePt])
@@index([channelId])
@@index([actionId])
}
// ─── AUDIT LOG ────────────────────────────────────────────
model AuditLog {
id String @id @default(cuid())
actorId String
entityType String
entityId String
action String
beforeJson Json?
afterJson Json?
requestId String?
createdAt DateTime @default(now())
@@index([entityType, entityId])
@@index([createdAt])
}