mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
442 lines
12 KiB
Plaintext
442 lines
12 KiB
Plaintext
// Prisma Schema for OpenFrame - Video Feedback Platform
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
// ============================================
|
|
// AUTH MODELS (NextAuth.js compatible)
|
|
// ============================================
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
name String?
|
|
email String? @unique
|
|
emailVerified DateTime?
|
|
image String?
|
|
password String? // Hashed password for email/password auth
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
accounts Account[]
|
|
sessions Session[]
|
|
ownedWorkspaces Workspace[]
|
|
workspaceMemberships WorkspaceMember[]
|
|
projects Project[]
|
|
comments Comment[]
|
|
projectMemberships ProjectMember[]
|
|
notificationSetting NotificationSetting?
|
|
watchProgress WatchProgress[]
|
|
|
|
@@map("users")
|
|
}
|
|
|
|
model Account {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
type String
|
|
provider String
|
|
providerAccountId String
|
|
refresh_token String? @db.Text
|
|
access_token String? @db.Text
|
|
expires_at Int?
|
|
token_type String?
|
|
scope String?
|
|
id_token String? @db.Text
|
|
session_state String?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([provider, providerAccountId])
|
|
@@map("accounts")
|
|
}
|
|
|
|
model Session {
|
|
id String @id @default(cuid())
|
|
sessionToken String @unique
|
|
userId String
|
|
expires DateTime
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("sessions")
|
|
}
|
|
|
|
model VerificationToken {
|
|
identifier String
|
|
token String @unique
|
|
expires DateTime
|
|
|
|
@@unique([identifier, token])
|
|
@@map("verification_tokens")
|
|
}
|
|
|
|
// ============================================
|
|
// APPLICATION MODELS
|
|
// ============================================
|
|
|
|
// ---- Workspace ----
|
|
|
|
model Workspace {
|
|
id String @id @default(cuid())
|
|
name String
|
|
slug String @unique
|
|
description String? @db.Text
|
|
|
|
// Ownership
|
|
ownerId String
|
|
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
members WorkspaceMember[]
|
|
projects Project[]
|
|
|
|
@@index([ownerId])
|
|
@@index([slug])
|
|
@@map("workspaces")
|
|
}
|
|
|
|
model WorkspaceMember {
|
|
id String @id @default(cuid())
|
|
role WorkspaceMemberRole @default(COMMENTATOR)
|
|
|
|
workspaceId String
|
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([workspaceId, userId])
|
|
@@index([userId])
|
|
@@map("workspace_members")
|
|
}
|
|
|
|
enum WorkspaceMemberRole {
|
|
ADMIN // Full access: manage members, delete projects, etc.
|
|
COMMENTATOR // Can view all projects and comment only
|
|
}
|
|
|
|
// ---- Project ----
|
|
|
|
model Project {
|
|
id String @id @default(cuid())
|
|
name String
|
|
description String? @db.Text
|
|
slug String @unique // URL-friendly identifier
|
|
|
|
// Visibility
|
|
visibility ProjectVisibility @default(PRIVATE)
|
|
|
|
// Ownership
|
|
ownerId String
|
|
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
|
|
|
// Workspace (required - every project belongs to a workspace)
|
|
workspaceId String
|
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
videos Video[]
|
|
members ProjectMember[]
|
|
shareLinks ShareLink[]
|
|
commentTags CommentTag[]
|
|
|
|
@@index([ownerId])
|
|
@@index([slug])
|
|
@@index([workspaceId])
|
|
@@index([workspaceId, updatedAt(sort: Desc)])
|
|
@@map("projects")
|
|
}
|
|
|
|
enum ProjectVisibility {
|
|
PRIVATE // Only owner can access
|
|
INVITE // Owner + specifically invited members
|
|
PUBLIC // Anyone with the link can access
|
|
}
|
|
|
|
model ProjectMember {
|
|
id String @id @default(cuid())
|
|
role ProjectMemberRole @default(COMMENTATOR)
|
|
|
|
projectId String
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([projectId, userId])
|
|
@@index([userId])
|
|
@@map("project_members")
|
|
}
|
|
|
|
enum ProjectMemberRole {
|
|
ADMIN // Can manage members, settings, delete videos
|
|
COMMENTATOR // Can view and comment only
|
|
}
|
|
|
|
model Video {
|
|
id String @id @default(cuid())
|
|
title String
|
|
description String? @db.Text
|
|
|
|
// Ordering within project
|
|
position Int @default(0)
|
|
|
|
// Project relation
|
|
projectId String
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
versions VideoVersion[]
|
|
|
|
@@index([projectId])
|
|
@@map("videos")
|
|
}
|
|
|
|
model VideoVersion {
|
|
id String @id @default(cuid())
|
|
versionNumber Int // 1, 2, 3...
|
|
versionLabel String? // Optional label like "Final Cut", "Review Draft"
|
|
|
|
// Video source (future-proof for multiple providers)
|
|
providerId String // 'youtube', 'vimeo', 'direct'
|
|
videoId String // Provider-specific video ID
|
|
originalUrl String // Original URL submitted by user
|
|
|
|
// Cached metadata
|
|
title String?
|
|
thumbnailUrl String?
|
|
duration Int? // Duration in seconds
|
|
|
|
// Status
|
|
isActive Boolean @default(true) // Currently displayed version
|
|
|
|
// Parent video
|
|
videoParentId String
|
|
video Video @relation(fields: [videoParentId], references: [id], onDelete: Cascade)
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
|
|
// Relations
|
|
comments Comment[]
|
|
watchProgress WatchProgress[]
|
|
|
|
@@unique([videoParentId, versionNumber])
|
|
@@index([videoParentId])
|
|
@@index([videoParentId, isActive])
|
|
@@map("video_versions")
|
|
}
|
|
|
|
model Comment {
|
|
id String @id @default(cuid())
|
|
|
|
// Comment content
|
|
content String? @db.Text // Text content (null if voice-only)
|
|
|
|
// Timestamp in video (in seconds, with decimal for precision)
|
|
timestamp Float // e.g., 65.5 = 1:05.5
|
|
timestampEnd Float? // Optional end timestamp for range comments
|
|
|
|
// Voice recording (optional)
|
|
voiceUrl String? // URL to voice recording file
|
|
voiceDuration Float? // Duration of voice recording in seconds
|
|
|
|
// Threading
|
|
parentId String?
|
|
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade)
|
|
replies Comment[] @relation("CommentReplies")
|
|
|
|
// Status
|
|
isResolved Boolean @default(false)
|
|
resolvedAt DateTime?
|
|
|
|
// Author (optional for guest comments)
|
|
authorId String?
|
|
author User? @relation(fields: [authorId], references: [id], onDelete: SetNull)
|
|
|
|
// Guest author info (when authorId is null)
|
|
guestName String?
|
|
guestEmail String?
|
|
|
|
// Video version relation
|
|
versionId String
|
|
version VideoVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
|
|
|
|
// Comment tag (colored category)
|
|
tagId String?
|
|
tag CommentTag? @relation(fields: [tagId], references: [id], onDelete: SetNull)
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([versionId])
|
|
@@index([parentId])
|
|
@@index([authorId])
|
|
@@index([timestamp])
|
|
@@index([tagId])
|
|
@@index([versionId, isResolved, timestamp])
|
|
@@index([versionId, parentId, createdAt])
|
|
@@map("comments")
|
|
}
|
|
|
|
model CommentTag {
|
|
id String @id @default(cuid())
|
|
name String // e.g., "Feedback", "Technical", "Urgent"
|
|
color String // Hex color, e.g., "#3B82F6"
|
|
|
|
// Project relation (tags are per-project)
|
|
projectId String
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
// Position for ordering in UI
|
|
position Int @default(0)
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
comments Comment[]
|
|
|
|
@@unique([projectId, name])
|
|
@@index([projectId])
|
|
@@map("comment_tags")
|
|
}
|
|
|
|
model ShareLink {
|
|
id String @id @default(cuid())
|
|
token String @unique // Random token for URL
|
|
|
|
// What is being shared
|
|
projectId String
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
// Permissions
|
|
permission SharePermission @default(VIEW)
|
|
|
|
// Optional restrictions
|
|
expiresAt DateTime? // Link expiration
|
|
maxUses Int? // Maximum number of uses
|
|
useCount Int @default(0)
|
|
passwordHash String? // Bcrypt hash of optional password protection
|
|
|
|
// Settings
|
|
allowGuests Boolean @default(true) // Allow comments without account
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([projectId])
|
|
@@index([token])
|
|
@@index([token, expiresAt])
|
|
@@map("share_links")
|
|
}
|
|
|
|
enum SharePermission {
|
|
VIEW // Can only view
|
|
COMMENT // Can view and comment
|
|
}
|
|
|
|
// ============================================
|
|
// NOTIFICATION SETTINGS
|
|
// ============================================
|
|
|
|
model NotificationSetting {
|
|
id String @id @default(cuid())
|
|
|
|
userId String @unique
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
// Telegram webhook
|
|
telegramBotToken String? // Bot token from @BotFather
|
|
telegramChatId String? // Chat/group ID to send messages to
|
|
telegramEnabled Boolean @default(false)
|
|
|
|
// Email notifications (uses account email by default)
|
|
emailEnabled Boolean @default(false)
|
|
|
|
// Event subscriptions
|
|
onNewVideo Boolean @default(true)
|
|
onNewVersion Boolean @default(true)
|
|
onNewComment Boolean @default(true)
|
|
onNewReply Boolean @default(true)
|
|
|
|
// User timezone for notification timestamps (IANA timezone identifier)
|
|
timezone String @default("UTC")
|
|
|
|
// Timestamps
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@map("notification_settings")
|
|
}
|
|
|
|
// ============================================
|
|
// WATCH PROGRESS
|
|
// ============================================
|
|
|
|
model WatchProgress {
|
|
id String @id @default(cuid())
|
|
|
|
// User relation (optional for guest progress, though typically requires auth)
|
|
userId String
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
// Video version relation
|
|
versionId String
|
|
version VideoVersion @relation(fields: [versionId], references: [id], onDelete: Cascade)
|
|
|
|
// Progress data
|
|
progress Float // Current playback position in seconds
|
|
duration Float // Total video duration at time of save
|
|
percentage Float // Progress as percentage (0-100)
|
|
|
|
// Timestamps
|
|
updatedAt DateTime @updatedAt
|
|
createdAt DateTime @default(now())
|
|
|
|
// One progress record per user per version
|
|
@@unique([userId, versionId])
|
|
@@index([userId])
|
|
@@index([versionId])
|
|
@@map("watch_progress")
|
|
}
|
|
|
|
// Rate limiting table (created as UNLOGGED via raw SQL migration)
|
|
// Defined here so `prisma db push` doesn't drop it
|
|
model RateLimit {
|
|
id Int @id @default(autoincrement())
|
|
key String @db.VarChar(255)
|
|
action String @db.VarChar(50)
|
|
count Int @default(1)
|
|
windowStart DateTime @default(now()) @map("window_start")
|
|
|
|
@@unique([key, action])
|
|
@@index([key, action])
|
|
@@index([windowStart])
|
|
@@map("rate_limits")
|
|
}
|