mirror of
https://github.com/mfts/papermark.git
synced 2025-12-20 01:03:24 +08:00
534 lines
17 KiB
Plaintext
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
|
|
}
|