Files
echoboard/packages/api/prisma/schema.prisma

496 lines
14 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum AuthMethod {
COOKIE
PASSKEY
}
enum PostType {
FEATURE_REQUEST
BUG_REPORT
}
model Board {
id String @id @default(cuid())
slug String @unique
name String
description String?
externalUrl String?
iconName String?
iconColor String?
isArchived Boolean @default(false)
voteBudget Int @default(10)
voteBudgetReset String @default("monthly")
lastBudgetReset DateTime?
allowMultiVote Boolean @default(false)
rssEnabled Boolean @default(true)
rssFeedCount Int @default(50)
staleDays Int @default(0)
position Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
activityEvents ActivityEvent[]
pushSubscriptions PushSubscription[]
statusConfig BoardStatus[]
templates BoardTemplate[]
changelogEntries ChangelogEntry[]
}
model BoardStatus {
id String @id @default(cuid())
boardId String
status String
label String
color String
position Int @default(0)
enabled Boolean @default(true)
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
@@unique([boardId, status])
@@index([boardId, position])
}
model User {
id String @id @default(cuid())
authMethod AuthMethod @default(COOKIE)
tokenHash String? @unique
username String?
usernameIdx String? @unique
displayName String?
avatarPath String?
darkMode String @default("system")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
passkeys Passkey[]
posts Post[]
comments Comment[]
reactions Reaction[]
votes Vote[]
notifications Notification[]
pushSubscriptions PushSubscription[]
attachments Attachment[]
adminLink AdminUser?
edits EditHistory[]
recoveryCode RecoveryCode?
}
model RecoveryCode {
id String @id @default(cuid())
codeHash String
phraseIdx String @unique
userId String @unique
expiresAt DateTime
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([expiresAt])
}
model Passkey {
id String @id @default(cuid())
credentialId String
credentialIdIdx String @unique
credentialPublicKey Bytes
counter BigInt
credentialDeviceType String
credentialBackedUp Boolean
transports String?
userId String
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model Post {
id String @id @default(cuid())
type PostType
title String
description Json
status String @default("OPEN")
statusReason String?
category String?
templateId String?
voteCount Int @default(0)
viewCount Int @default(0)
isPinned Boolean @default(false)
isEditLocked Boolean @default(false)
isThreadLocked Boolean @default(false)
isVotingLocked Boolean @default(false)
onBehalfOf String?
boardId String
authorId String
lastActivityAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
statusChanges StatusChange[]
comments Comment[]
votes Vote[]
adminResponses AdminResponse[]
adminNotes AdminNote[]
notifications Notification[]
activityEvents ActivityEvent[]
pushSubscriptions PushSubscription[]
tags PostTag[]
attachments Attachment[]
editHistory EditHistory[]
@@index([boardId, status])
}
model StatusChange {
id String @id @default(cuid())
postId String
fromStatus String
toStatus String
reason String?
changedBy String
createdAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
}
model Comment {
id String @id @default(cuid())
body String
postId String
authorId String
replyToId String?
isAdmin Boolean @default(false)
isEditLocked Boolean @default(false)
adminUserId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
replyTo Comment? @relation("CommentReplies", fields: [replyToId], references: [id], onDelete: SetNull)
replies Comment[] @relation("CommentReplies")
adminUser AdminUser? @relation(fields: [adminUserId], references: [id], onDelete: SetNull)
reactions Reaction[]
attachments Attachment[]
editHistory EditHistory[]
@@index([replyToId])
@@index([postId, createdAt])
}
model Reaction {
id String @id @default(cuid())
emoji String
commentId String
userId String
createdAt DateTime @default(now())
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([commentId, userId, emoji])
}
model Vote {
id String @id @default(cuid())
weight Int @default(1)
importance String?
postId String
voterId String
budgetPeriod String
createdAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
voter User @relation(fields: [voterId], references: [id], onDelete: Cascade)
@@unique([postId, voterId])
}
enum TeamRole {
SUPER_ADMIN
ADMIN
MODERATOR
}
model AdminUser {
id String @id @default(cuid())
email String? @unique
passwordHash String?
role TeamRole @default(ADMIN)
displayName String?
teamTitle String?
linkedUserId String? @unique
invitedById String?
createdAt DateTime @default(now())
linkedUser User? @relation(fields: [linkedUserId], references: [id], onDelete: SetNull)
invitedBy AdminUser? @relation("TeamInviter", fields: [invitedById], references: [id], onDelete: SetNull)
invitees AdminUser[] @relation("TeamInviter")
responses AdminResponse[]
notes AdminNote[]
comments Comment[]
createdInvites TeamInvite[] @relation("InviteCreator")
claimedInvite TeamInvite? @relation("InviteClaimer")
}
model TeamInvite {
id String @id @default(cuid())
tokenHash String @unique
role TeamRole
label String?
expiresAt DateTime
createdById String
claimedById String? @unique
claimedAt DateTime?
recoveryHash String?
recoveryIdx String?
createdAt DateTime @default(now())
createdBy AdminUser @relation("InviteCreator", fields: [createdById], references: [id], onDelete: Cascade)
claimedBy AdminUser? @relation("InviteClaimer", fields: [claimedById], references: [id], onDelete: SetNull)
@@index([expiresAt])
}
model AdminResponse {
id String @id @default(cuid())
body String
postId String
adminId String
createdAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
admin AdminUser @relation(fields: [adminId], references: [id], onDelete: Cascade)
}
model ActivityEvent {
id String @id @default(cuid())
type String
boardId String
postId String?
metadata Json
createdAt DateTime @default(now())
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
post Post? @relation(fields: [postId], references: [id], onDelete: SetNull)
@@index([boardId, createdAt])
@@index([createdAt])
}
model PushSubscription {
id String @id @default(cuid())
endpoint String
endpointIdx String @unique
keysP256dh String
keysAuth String
userId String
boardId String?
postId String?
failureCount Int @default(0)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
board Board? @relation(fields: [boardId], references: [id], onDelete: Cascade)
post Post? @relation(fields: [postId], references: [id], onDelete: SetNull)
}
model Category {
id String @id @default(cuid())
name String @unique
slug String @unique
createdAt DateTime @default(now())
}
model Notification {
id String @id @default(cuid())
type String
title String
body String
postId String?
userId String
read Boolean @default(false)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
post Post? @relation(fields: [postId], references: [id], onDelete: SetNull)
@@index([userId, read, createdAt])
}
model Webhook {
id String @id @default(cuid())
url String
secret String
events String[] @default(["status_changed", "post_created", "comment_added"])
active Boolean @default(true)
createdAt DateTime @default(now())
}
model PostMerge {
id String @id @default(cuid())
sourcePostId String
targetPostId String
mergedBy String
createdAt DateTime @default(now())
@@index([sourcePostId])
}
model ChangelogEntry {
id String @id @default(cuid())
title String
body String
boardId String?
publishedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
board Board? @relation(fields: [boardId], references: [id], onDelete: Cascade)
@@index([boardId, publishedAt])
}
model AdminNote {
id String @id @default(cuid())
body String
postId String
adminId String
createdAt DateTime @default(now())
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
admin AdminUser @relation(fields: [adminId], references: [id], onDelete: Cascade)
}
model Tag {
id String @id @default(cuid())
name String @unique
color String @default("#6366F1")
createdAt DateTime @default(now())
posts PostTag[]
}
model PostTag {
postId String
tagId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([postId, tagId])
}
model Attachment {
id String @id @default(cuid())
filename String
path String
size Int
mimeType String
postId String?
commentId String?
uploaderId String
createdAt DateTime @default(now())
post Post? @relation(fields: [postId], references: [id], onDelete: Cascade)
comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
uploader User @relation(fields: [uploaderId], references: [id], onDelete: Cascade)
}
model BoardTemplate {
id String @id @default(cuid())
boardId String
name String
fields Json
isDefault Boolean @default(false)
position Int @default(0)
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
@@index([boardId, position])
}
model SiteSettings {
id String @id @default("default")
appName String @default("Echoboard")
logoUrl String?
faviconUrl String?
accentColor String @default("#F59E0B")
headerFont String?
bodyFont String?
poweredByVisible Boolean @default(true)
customCss String?
ogImageUrl String?
ogDescription String?
updatedAt DateTime @updatedAt
}
model BlockedToken {
id String @id @default(cuid())
tokenHash String @unique
expiresAt DateTime
createdAt DateTime @default(now())
@@index([expiresAt])
}
model EditHistory {
id String @id @default(cuid())
postId String?
commentId String?
editedBy String?
previousTitle String?
previousDescription Json?
previousBody String?
createdAt DateTime @default(now())
post Post? @relation(fields: [postId], references: [id], onDelete: Cascade)
comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
editor User? @relation(fields: [editedBy], references: [id], onDelete: SetNull)
@@index([postId, createdAt])
@@index([commentId, createdAt])
}
model Plugin {
id String @id @default(cuid())
name String @unique
version String
description String?
author String?
enabled Boolean @default(false)
dirPath String
entryPoint String @default("index.js")
config Json @default("{}")
configSchema Json @default("[]")
installedAt DateTime @default(now())
updatedAt DateTime @updatedAt
data PluginData[]
}
model PluginData {
id String @id @default(cuid())
pluginId String
key String
value Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
plugin Plugin @relation(fields: [pluginId], references: [id], onDelete: Cascade)
@@unique([pluginId, key])
@@index([pluginId])
}