Initial commit: YouTube Studio Flow (backend, frontend, infrastructure, docs)
This commit is contained in:
@@ -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])
|
||||
}
|
||||
Reference in New Issue
Block a user