Files
plp/prisma/schema.prisma
T

469 lines
19 KiB
Plaintext

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum AccountStatus {
ACTIVE
SUSPENDED
DELETED
}
enum AuthProvider {
PHONE
APPLE
WECHAT
}
enum RefreshTokenStatus {
ACTIVE
USED
}
enum ReviewStatus {
DRAFT
REVIEWING
REJECTED
MANUAL_REVIEW
APPROVED
}
enum BottlePoolStatus {
IN_POOL
LEASED
CONSUMED
REMOVED
CLOSED
}
enum BottlePickLeaseStatus {
ACTIVE
RETURNED
EXPIRED
CONSUMED
}
enum ConversationStatus {
ACTIVE
CLOSED
}
enum MessageStatus {
SENT
DELETED
MODERATED
}
enum ReportStatus {
PENDING
REVIEWING
RESOLVED
DISMISSED
}
enum ModerationTaskStatus {
PENDING
PROCESSING
COMPLETED
}
enum SanctionType {
WARNING
MUTE
SUSPENSION
BAN
}
enum NotificationStatus {
PENDING
SENT
READ
FAILED
}
enum OutboxStatus {
PENDING
PROCESSING
PUBLISHED
FAILED
}
model Account {
id String @id @default(uuid()) @db.Uuid
phoneCiphertext Bytes @map("phone_ciphertext")
phoneHmac String @unique @map("phone_hmac") @db.VarChar(128)
status AccountStatus @default(ACTIVE)
tokenVersion Int @default(0) @map("token_version")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
anonymousProfile AnonymousProfile?
authIdentities AuthIdentity[]
sessions Session[]
bottles Bottle[]
pickLeases BottlePickLease[] @relation("PickerLeases")
pickHistory BottlePickHistory[] @relation("PickerHistory")
conversationMembers ConversationMember[]
messages Message[]
blocksInitiated Block[] @relation("Blocker")
blocksReceived Block[] @relation("Blocked")
reportsFiled Report[] @relation("Reporter")
reportsAgainst Report[] @relation("Reported")
moderationTasks ModerationTask[] @relation("Moderator")
sanctionsReceived Sanction[] @relation("SanctionedAccount")
sanctionsIssued Sanction[] @relation("SanctionIssuer")
notifications Notification[]
auditLogs AuditLog[] @relation("AuditActor")
dailyUsage DailyUsage[]
@@map("accounts")
}
model AnonymousProfile {
/// Review queue index anonymous_profiles_review_status_updated_at_idx is managed in 0006 SQL.
id String @id @default(uuid()) @db.Uuid
accountId String @unique @map("account_id") @db.Uuid
publicId String @unique @default(uuid()) @map("public_id") @db.Uuid
nickname String @db.VarChar(64)
avatarKey String? @map("avatar_key") @db.VarChar(255)
avatarColor String @map("avatar_color") @db.VarChar(16)
bio String? @db.VarChar(500)
reviewStatus ReviewStatus @default(REVIEWING) @map("review_status")
version Int @default(1)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
@@index([reviewStatus, updatedAt], map: "anonymous_profiles_review_status_updated_at_idx")
@@map("anonymous_profiles")
}
model AuthIdentity {
id String @id @default(uuid()) @db.Uuid
accountId String @map("account_id") @db.Uuid
provider AuthProvider
providerSubject String @map("provider_subject") @db.VarChar(255)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
@@unique([provider, providerSubject])
@@index([accountId])
@@map("auth_identities")
}
model Session {
id String @id @default(uuid()) @db.Uuid
accountId String @map("account_id") @db.Uuid
tokenVersion Int @default(0) @map("token_version")
refreshTokenHash String @unique @map("refresh_token_hash") @db.VarChar(255)
deviceId String? @map("device_id") @db.VarChar(255)
expiresAt DateTime @map("expires_at") @db.Timestamptz(3)
revokedAt DateTime? @map("revoked_at") @db.Timestamptz(3)
tokenFamily String @map("token_family") @db.Uuid
generation Int @default(0)
previousTokenHash String? @unique @map("previous_token_hash") @db.VarChar(255)
rotatedAt DateTime? @map("rotated_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
refreshTokens RefreshToken[]
@@index([accountId, expiresAt])
@@index([tokenFamily])
@@map("sessions")
}
model RefreshToken {
id String @id @default(uuid()) @db.Uuid
tokenHash String @unique @map("token_hash") @db.VarChar(255)
sessionId String @map("session_id") @db.Uuid
generation Int
status RefreshTokenStatus @default(ACTIVE)
usedAt DateTime? @map("used_at") @db.Timestamptz(3)
replacedByHash String? @unique @map("replaced_by_hash") @db.VarChar(255)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
expiresAt DateTime @map("expires_at") @db.Timestamptz(3)
session Session @relation(fields: [sessionId], references: [id], onDelete: Cascade)
@@unique([sessionId, generation])
@@index([sessionId, status])
@@index([expiresAt])
@@map("refresh_tokens")
}
model Bottle {
id String @id @default(uuid()) @db.Uuid
authorId String @map("author_id") @db.Uuid
clientRequestId String @default(uuid()) @map("client_request_id") @db.VarChar(128)
contentText String @map("content_text") @db.Text
reviewStatus ReviewStatus @default(DRAFT) @map("review_status")
poolStatus BottlePoolStatus @default(CLOSED) @map("pool_status")
version Int @default(1)
approvedAt DateTime? @map("approved_at") @db.Timestamptz(3)
consumedAt DateTime? @map("consumed_at") @db.Timestamptz(3)
expiresAt DateTime? @map("expires_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
author Account @relation(fields: [authorId], references: [id], onDelete: Restrict)
leases BottlePickLease[]
pickHistory BottlePickHistory[]
conversation Conversation?
reports Report[]
@@unique([authorId, clientRequestId])
@@index([poolStatus, createdAt])
@@index([reviewStatus, createdAt])
@@index([authorId, createdAt])
@@map("bottles")
}
model BottlePickLease {
/// Partial unique index bottle_pick_leases_one_active_per_bottle is managed in 0001_init SQL.
id String @id @default(uuid()) @db.Uuid
bottleId String @map("bottle_id") @db.Uuid
pickerId String @map("picker_id") @db.Uuid
leaseTokenHash String @unique @map("lease_token_hash") @db.VarChar(255)
expiresAt DateTime @map("expires_at") @db.Timestamptz(3)
status BottlePickLeaseStatus @default(ACTIVE)
endedAt DateTime? @map("ended_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
bottle Bottle @relation(fields: [bottleId], references: [id], onDelete: Cascade)
picker Account @relation("PickerLeases", fields: [pickerId], references: [id], onDelete: Cascade)
@@index([pickerId, expiresAt])
@@index([bottleId, expiresAt])
@@map("bottle_pick_leases")
}
model BottlePickHistory {
id String @id @default(uuid()) @db.Uuid
bottleId String @map("bottle_id") @db.Uuid
pickerId String @map("picker_id") @db.Uuid
pickedAt DateTime @default(now()) @map("picked_at") @db.Timestamptz(3)
bottle Bottle @relation(fields: [bottleId], references: [id], onDelete: Cascade)
picker Account @relation("PickerHistory", fields: [pickerId], references: [id], onDelete: Cascade)
@@unique([bottleId, pickerId])
@@index([pickerId, pickedAt])
@@map("bottle_pick_history")
}
model DailyUsage {
/// Nonnegative counters are enforced by CHECK constraints in 0001_init SQL.
id String @id @default(uuid()) @db.Uuid
accountId String @map("account_id") @db.Uuid
usageDate DateTime @map("usage_date") @db.Date
bottlesCreated Int @default(0) @map("bottles_created")
bottlesPicked Int @default(0) @map("bottles_picked")
messagesSent Int @default(0) @map("messages_sent")
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
@@unique([accountId, usageDate])
@@index([usageDate])
@@map("daily_usage")
}
model Conversation {
id String @id @default(uuid()) @db.Uuid
sourceBottleId String @unique @map("source_bottle_id") @db.Uuid
status ConversationStatus @default(ACTIVE)
nextSeq BigInt @default(1) @map("next_seq")
lastMessageAt DateTime @default(now()) @map("last_message_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
sourceBottle Bottle @relation(fields: [sourceBottleId], references: [id], onDelete: Restrict)
members ConversationMember[]
messages Message[]
reports Report[]
@@index([status, updatedAt])
@@map("conversations")
}
model ConversationMember {
id String @id @default(uuid()) @db.Uuid
conversationId String @map("conversation_id") @db.Uuid
accountId String @map("account_id") @db.Uuid
joinedAt DateTime @default(now()) @map("joined_at") @db.Timestamptz(3)
leftAt DateTime? @map("left_at") @db.Timestamptz(3)
lastReadSeq BigInt @default(0) @map("last_read_seq")
peerAliasSnapshot String @map("peer_alias_snapshot") @db.VarChar(64)
blockedAt DateTime? @map("blocked_at") @db.Timestamptz(3)
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
@@unique([conversationId, accountId])
@@index([accountId, joinedAt])
@@map("conversation_members")
}
model Message {
id String @id @default(uuid()) @db.Uuid
conversationId String @map("conversation_id") @db.Uuid
senderId String @map("sender_id") @db.Uuid
clientMsgId String @map("client_msg_id") @db.VarChar(128)
seq BigInt
contentText String @map("content_text") @db.Text
status MessageStatus @default(SENT)
reviewStatus ReviewStatus @default(REVIEWING) @map("review_status")
sentAt DateTime @default(now()) @map("sent_at") @db.Timestamptz(3)
recalledAt DateTime? @map("recalled_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
sender Account @relation(fields: [senderId], references: [id], onDelete: Restrict)
reports Report[]
@@unique([conversationId, clientMsgId])
@@unique([conversationId, seq])
@@index([senderId, createdAt])
@@map("messages")
}
model Block {
id String @id @default(uuid()) @db.Uuid
blockerId String @map("blocker_id") @db.Uuid
blockedId String @map("blocked_id") @db.Uuid
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
blocker Account @relation("Blocker", fields: [blockerId], references: [id], onDelete: Cascade)
blocked Account @relation("Blocked", fields: [blockedId], references: [id], onDelete: Cascade)
@@unique([blockerId, blockedId])
@@index([blockedId])
@@map("blocks")
}
model Report {
/// Exactly one nullable target foreign key must be set; enforced in 0001_init SQL.
id String @id @default(uuid()) @db.Uuid
reporterId String @map("reporter_id") @db.Uuid
reportedAccountId String? @map("reported_account_id") @db.Uuid
bottleId String? @map("bottle_id") @db.Uuid
conversationId String? @map("conversation_id") @db.Uuid
messageId String? @map("message_id") @db.Uuid
reason String @db.VarChar(100)
details String? @db.Text
targetSnapshot Json @map("target_snapshot")
resolution String? @db.Text
status ReportStatus @default(PENDING)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
reporter Account @relation("Reporter", fields: [reporterId], references: [id], onDelete: Restrict)
reportedAccount Account? @relation("Reported", fields: [reportedAccountId], references: [id], onDelete: SetNull)
bottle Bottle? @relation(fields: [bottleId], references: [id], onDelete: SetNull)
conversation Conversation? @relation(fields: [conversationId], references: [id], onDelete: SetNull)
message Message? @relation(fields: [messageId], references: [id], onDelete: SetNull)
moderationTasks ModerationTask[]
@@index([status, createdAt])
@@index([reportedAccountId, createdAt])
@@map("reports")
}
model ModerationTask {
id String @id @default(uuid()) @db.Uuid
targetType String @map("target_type") @db.VarChar(50)
targetId String @map("target_id") @db.Uuid
provider String @db.VarChar(100)
result Json?
riskLabels String[] @map("risk_labels")
payloadHash String @map("payload_hash") @db.VarChar(128)
reportId String? @map("report_id") @db.Uuid
assignedToId String? @map("assigned_to_id") @db.Uuid
status ModerationTaskStatus @default(PENDING)
decision String? @db.Text
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
reviewedAt DateTime? @map("reviewed_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
report Report? @relation(fields: [reportId], references: [id], onDelete: SetNull)
assignedTo Account? @relation("Moderator", fields: [assignedToId], references: [id], onDelete: SetNull)
sanctions Sanction[]
@@unique([targetType, targetId])
@@index([status, createdAt])
@@index([targetType, targetId])
@@index([assignedToId, status])
@@map("moderation_tasks")
}
model Sanction {
id String @id @default(uuid()) @db.Uuid
accountId String @map("account_id") @db.Uuid
issuedById String? @map("issued_by_id") @db.Uuid
moderationTaskId String? @map("moderation_task_id") @db.Uuid
type SanctionType
reason String @db.Text
startsAt DateTime @default(now()) @map("starts_at") @db.Timestamptz(3)
expiresAt DateTime? @map("expires_at") @db.Timestamptz(3)
revokedAt DateTime? @map("revoked_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
account Account @relation("SanctionedAccount", fields: [accountId], references: [id], onDelete: Cascade)
issuedBy Account? @relation("SanctionIssuer", fields: [issuedById], references: [id], onDelete: SetNull)
moderationTask ModerationTask? @relation(fields: [moderationTaskId], references: [id], onDelete: SetNull)
@@index([accountId, expiresAt])
@@map("sanctions")
}
model Notification {
id String @id @default(uuid()) @db.Uuid
accountId String @map("account_id") @db.Uuid
type String @db.VarChar(100)
payload Json
status NotificationStatus @default(PENDING)
sentAt DateTime? @map("sent_at") @db.Timestamptz(3)
readAt DateTime? @map("read_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
@@index([accountId, status, createdAt])
@@map("notifications")
}
model OutboxEvent {
/// Processing lease index outbox_events_processing_lease_idx is managed in 0005 SQL.
id String @id @default(uuid()) @db.Uuid
aggregateType String @map("aggregate_type") @db.VarChar(100)
aggregateId String @map("aggregate_id") @db.Uuid
eventType String @map("event_type") @db.VarChar(150)
dedupeKey String @unique @default(uuid()) @map("dedupe_key") @db.VarChar(255)
payload Json
status OutboxStatus @default(PENDING)
attempts Int @default(0)
nextRetryAt DateTime @default(now()) @map("next_retry_at") @db.Timestamptz(3)
lockedAt DateTime? @map("locked_at") @db.Timestamptz(3)
publishedAt DateTime? @map("published_at") @db.Timestamptz(3)
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(3)
@@index([status, nextRetryAt])
@@index([status, lockedAt], map: "outbox_events_processing_lease_idx")
@@index([aggregateType, aggregateId])
@@map("outbox_events")
}
model AuditLog {
id String @id @default(uuid()) @db.Uuid
actorId String? @map("actor_id") @db.Uuid
action String @db.VarChar(150)
entityType String @map("entity_type") @db.VarChar(100)
entityId String? @map("entity_id") @db.Uuid
requestId String? @map("request_id") @db.VarChar(128)
ipHash String? @map("ip_hash") @db.VarChar(128)
metadata Json?
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
actor Account? @relation("AuditActor", fields: [actorId], references: [id], onDelete: SetNull)
@@index([actorId, createdAt])
@@index([entityType, entityId, createdAt])
@@index([requestId])
@@map("audit_logs")
}