Files
papermark/prisma/schema/schema.prisma
2025-12-09 17:53:10 +01:00

534 lines
17 KiB
Plaintext

datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL") // uses connection pooling
directUrl = env("POSTGRES_PRISMA_URL_NON_POOLING") // uses a direct connection
shadowDatabaseUrl = env("POSTGRES_PRISMA_SHADOW_URL") // used for migrations
}
generator client {
provider = "prisma-client-js"
previewFeatures = ["relationJoins", "prismaSchemaFolder"]
}
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])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
createdAt DateTime @default(now())
accounts Account[]
sessions Session[]
documents Document[]
teams UserTeam[]
domains Domain[]
chats Chat[]
contactId String?
plan String @default("free")
stripeId String? @unique // Stripe subscription / customer ID
subscriptionId String? @unique // Stripe subscription ID
startsAt DateTime? // Stripe subscription start date
endsAt DateTime? // Stripe subscription end date
restrictedTokens RestrictedToken[]
// conversation
participatedConversations ConversationParticipant[]
messages Message[]
// FAQ system
publishedFaqItems DataroomFaqItem[]
createdAnnotations DocumentAnnotation[] // Annotations created by this user
installedIntegrations InstalledIntegration[]
}
model Brand {
id String @id @default(cuid())
logo String? // This should be a reference to where the file is stored (S3, Google Cloud Storage, etc.)
banner String? // Banner image for dataroom view (fallback)
brandColor String? // This should be a reference to the brand color
accentColor String? // This should be a reference to the accent color
welcomeMessage String? // This should be a reference to the welcome message
teamId String @unique
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Domain {
id String @id @default(cuid())
slug String @unique
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
userId String?
teamId String
Team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
verified Boolean @default(false) // Whether the domain has been verified
isDefault Boolean @default(false) // Whether the domain is the primary domain
lastChecked DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
links Link[] // links associated with this domain
@@index([userId])
@@index([teamId])
}
model View {
id String @id @default(cuid())
link Link @relation(fields: [linkId], references: [id], onDelete: Cascade)
linkId String
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
documentId String?
dataroom Dataroom? @relation(fields: [dataroomId], references: [id], onDelete: Cascade)
dataroomId String?
dataroomViewId String? // This is the view ID from the dataroom
viewerEmail String? // Email of the viewer if known
viewerName String? // Name of the viewer if known
verified Boolean @default(false) // Whether the viewer email has been verified
viewedAt DateTime @default(now())
downloadedAt DateTime? // This is the time the document was downloaded
downloadType DownloadType? // Type of download: SINGLE, BULK, or FOLDER
downloadMetadata Json? // Metadata about the download (folder name, document list, etc.)
reactions Reaction[]
viewType ViewType @default(DOCUMENT_VIEW)
viewerId String? // This is the viewer ID from the dataroom
viewer Viewer? @relation(fields: [viewerId], references: [id], onDelete: Cascade)
groupId String? // This is the group ID from the dataroom
group ViewerGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)
feedbackResponse FeedbackResponse?
agreementResponse AgreementResponse?
customFieldResponse CustomFieldResponse?
isArchived Boolean @default(false) // Indicates if the view is archived and not counted in the analytics
// conversation
conversationViews ConversationView[]
messages Message[]
initialConversations Conversation[] @relation("initialView")
uploadedDocuments DocumentUpload[] // uploaded documents by this view
// AI chats
chats Chat[]
teamId String?
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
@@index([linkId])
@@index([documentId])
@@index([dataroomId])
@@index([dataroomViewId])
@@index([viewerId])
@@index([groupId]) // Performance optimization for groupBy queries on groupId
@@index([teamId])
@@index([viewedAt(sort: Desc)]) // Performance optimization for date aggregations
@@index([viewerId, documentId]) // Performance optimization for joins with filtering
@@index([viewerEmail]) // Performance optimization for viewer email filtering
@@index([documentId, isArchived]) // Performance optimization for active views filtering
@@index([documentId, viewedAt(sort: Desc)]) // Performance optimization for latest views queries
}
enum ViewType {
DOCUMENT_VIEW
DATAROOM_VIEW
}
enum DownloadType {
SINGLE // Individual document download
BULK // Full dataroom bulk download
FOLDER // Folder download
}
model Viewer {
id String @id @default(cuid())
email String
verified Boolean @default(false) // Whether the viewer email has been verified
invitedAt DateTime? // This is the time the viewer was invited
notificationPreferences Json? // Format: { dataroom: {"dr_123": { "enabled": false }, "dr_456": { "enabled": true } } } }
dataroomId String?
dataroom Dataroom? @relation(fields: [dataroomId], references: [id], onDelete: SetNull)
teamId String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
views View[]
groups ViewerGroupMembership[]
invitations ViewerInvitation[]
participatedConversations ConversationParticipant[]
messages Message[]
uploadedDocuments DocumentUpload[] // uploaded documents by this viewer
// AI chats
chats Chat[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([teamId, email])
@@index([teamId])
@@index([dataroomId])
}
model Reaction {
id String @id @default(cuid())
view View @relation(fields: [viewId], references: [id], onDelete: Cascade)
viewId String
pageNumber Int
type String // e.g., "like", "dislike", "love", "hate", etc.
createdAt DateTime @default(now())
@@index([viewId])
@@index([viewId, type]) // Performance optimization for reaction grouping
}
model Invitation {
email String
expires DateTime
teamId String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
token String @unique
@@unique([email, teamId])
}
enum EmailType {
FIRST_DAY_DOMAIN_REMINDER_EMAIL
FIRST_DOMAIN_INVALID_EMAIL
SECOND_DOMAIN_INVALID_EMAIL
FIRST_TRIAL_END_REMINDER_EMAIL
FINAL_TRIAL_END_REMINDER_EMAIL
}
model SentEmail {
id String @id @default(cuid())
type EmailType
recipient String // Email address of the recipient
marketing Boolean @default(false)
createdAt DateTime @default(now())
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
teamId String
domainSlug String? // Domain that triggered the email. This can be nullable, representing emails not triggered by domains
@@index([teamId])
}
model Chat {
id String @id @default(cuid())
title String? // Generated title from first message
// Context associations
teamId String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
documentId String?
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
dataroomId String?
dataroom Dataroom? @relation(fields: [dataroomId], references: [id], onDelete: Cascade)
linkId String?
link Link? @relation(fields: [linkId], references: [id], onDelete: Cascade)
viewId String?
view View? @relation(fields: [viewId], references: [id], onDelete: Cascade)
// User associations (internal or external)
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
viewerId String?
viewer Viewer? @relation(fields: [viewerId], references: [id], onDelete: Cascade)
// OpenAI references
vectorStoreId String? // The vector store used for this chat
messages ChatMessage[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lastMessageAt DateTime? // Track latest activity
@@index([teamId])
@@index([documentId])
@@index([dataroomId])
@@index([linkId])
@@index([userId])
@@index([viewerId])
@@index([viewId])
@@index([createdAt(sort: Desc)])
}
model ChatMessage {
id String @id @default(cuid())
chatId String
chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
role String // "user" | "assistant" | "system"
content String @db.Text
// Optional structured data
metadata Json? // Store sources, page numbers, confidence scores, etc.
createdAt DateTime @default(now())
@@index([chatId])
@@index([chatId, createdAt])
}
model Feedback {
id String @id @default(cuid())
linkId String @unique
link Link @relation(fields: [linkId], references: [id], onDelete: Cascade)
data Json // This will store the feedback question data: {question: "What is the purpose of this document?", type: "yes/no", options: ["Yes", "No"]}
responses FeedbackResponse[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([linkId])
}
model FeedbackResponse {
id String @id @default(cuid())
feedbackId String
feedback Feedback @relation(fields: [feedbackId], references: [id], onDelete: Cascade)
data Json // This will store the feedback question data: {question: "What is the purpose of this document?", type: "yes/no", options: ["Yes", "No"], answer: "Yes"}
viewId String @unique
view View @relation(fields: [viewId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([feedbackId])
@@index([viewId])
}
model Agreement {
id String @id @default(cuid())
name String // Easily identifiable name for the agreement
content String // This will store the agreement content (URL or text)
contentType String @default("LINK") // "LINK" or "TEXT" - determines how content should be displayed
links Link[]
responses AgreementResponse[]
requireName Boolean @default(true) // Optional require name field
teamId String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
deletedBy String?
@@index([teamId])
}
model AgreementResponse {
id String @id @default(cuid())
agreementId String
agreement Agreement @relation(fields: [agreementId], references: [id], onDelete: Cascade)
viewId String @unique
view View @relation(fields: [viewId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([agreementId])
@@index([viewId])
}
model IncomingWebhook {
id String @id @default(cuid())
externalId String @unique
name String
secret String? // Webhook signing secret for verification
source String? // Allowed source URL/domain
actions String? // comma separated (Eg: "documents:write,documentVersions:write")
consecutiveFailures Int @default(0)
lastFailedAt DateTime?
disabledAt DateTime?
teamId String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([teamId])
}
model RestrictedToken {
id String @id @default(cuid())
name String
hashedKey String @unique
partialKey String
scopes String? // comma separated (Eg: "documents:write,links:write")
expires DateTime?
lastUsed DateTime?
rateLimit Int @default(60) // rate limit per minute
userId String
teamId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
@@index([teamId])
}
model Webhook {
id String @id @default(cuid())
pId String @unique // public ID for the webhook
name String
url String
secret String // signing secret for the webhook
triggers Json
teamId String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([teamId])
}
model YearInReview {
id String @id @default(cuid())
teamId String
status String @default("pending") // pending, processing, completed, failed
attempts Int @default(0)
lastAttempted DateTime?
error String?
stats Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status, attempts])
@@index([teamId])
}
enum TagType {
LINK_TAG
DOCUMENT_TAG
DATAROOM_TAG
}
model Tag {
id String @id @default(cuid())
name String
color String
description String?
teamId String
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
items TagItem[]
createdBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([teamId, name])
@@index([teamId])
@@index([name])
@@index([id])
}
model TagItem {
id String @id @default(cuid())
tagId String
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
itemType TagType
// tag can be linked to a link, document or dataroom
linkId String?
link Link? @relation(fields: [linkId], references: [id], onDelete: Cascade)
documentId String?
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
dataroomId String?
dataroom Dataroom? @relation(fields: [dataroomId], references: [id], onDelete: Cascade)
taggedBy String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([tagId, linkId])
@@index([tagId, documentId])
@@index([tagId, dataroomId])
}
model ViewerInvitation {
id String @id @default(cuid())
viewerId String
viewer Viewer @relation(fields: [viewerId], references: [id], onDelete: Cascade)
linkId String
link Link @relation(fields: [linkId], references: [id], onDelete: Cascade)
groupId String?
group ViewerGroup? @relation(fields: [groupId], references: [id], onDelete: SetNull)
invitedBy String
customMessage String?
sentAt DateTime @default(now())
status InvitationStatus @default(SENT)
createdAt DateTime @default(now())
@@index([viewerId])
@@index([linkId])
@@index([groupId])
}
enum InvitationStatus {
SENT
FAILED
BOUNCED
}