// This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique username String @unique passwordHash String displayName String? avatarUrl String? role String @default("user") // admin, user, moderator isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt oauth2Tokens OAuth2Token[] oauth2AuthCodes OAuth2AuthorizationCode[] articles Article[] comments Comment[] reactions Reaction[] @relation("UserReactions") follows Follow[] @relation("FollowFollower") followees Follow[] @relation("FollowFollowee") subscriptions Subscription[] @relation("SubscriptionSubscriber") targets Subscription[] @relation("SubscriptionTarget") notifications Notification[] @relation("UserNotifications") editedVersions ArticleVersion[] @relation("ArticleVersionEditor") reviewedCommentAudits CommentAudit[] @relation("CommentAuditReviewer") } model OAuth2Client { id Int @id @default(autoincrement()) clientId String @unique clientSecret String redirectUris String[] // JSON array of URIs scopes String @default("read write") createdAt DateTime @default(now()) tokens OAuth2Token[] authCodes OAuth2AuthorizationCode[] } model OAuth2AuthorizationCode { id Int @id @default(autoincrement()) codeHash String @unique clientId String userId Int? redirectUri String scopes String expiresAt DateTime used Boolean @default(false) createdAt DateTime @default(now()) client OAuth2Client @relation(fields: [clientId], references: [clientId]) user User? @relation(fields: [userId], references: [id]) } model OAuth2Token { id Int @id @default(autoincrement()) accessTokenHash String @unique refreshTokenHash String? @unique clientId String userId Int? scopes String expiresAt DateTime revoked Boolean @default(false) createdAt DateTime @default(now()) client OAuth2Client @relation(fields: [clientId], references: [clientId]) user User? @relation(fields: [userId], references: [id]) @@index([userId]) @@index([clientId]) } model Article { id Int @id @default(autoincrement()) slug String @unique title String content String? // Markdown content excerpt String? coverImageUrl String? status String @default("draft") // draft, published, archived visibility String @default("public") // public, unlisted, private passwordHash String? // for password-protected articles token String? @unique // for token-based access viewCount Int @default(0) // number of views publishedAt DateTime? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt authorId Int author User @relation(fields: [authorId], references: [id]) versions ArticleVersion[] @relation("ArticleVersions") comments Comment[] reactions Reaction[] @relation("ArticleReactions") linkAccesses LinkAccess[] @relation("ArticleLinkAccess") @@index([authorId]) } model ArticleVersion { id Int @id @default(autoincrement()) articleId Int version Int title String content String? excerpt String? editorId Int? createdAt DateTime @default(now()) article Article @relation("ArticleVersions", fields: [articleId], references: [id]) editor User? @relation("ArticleVersionEditor", fields: [editorId], references: [id]) @@unique([articleId, version]) } model Comment { id Int @id @default(autoincrement()) articleId Int parentId Int? content String status String @default("pending") // pending, approved, rejected, suspicious authorName String? authorEmail String? authorId Int? // registered user ipAddress String? userAgent String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt article Article @relation(fields: [articleId], references: [id]) parent Comment? @relation("CommentReplies", fields: [parentId], references: [id]) replies Comment[] @relation("CommentReplies") audits CommentAudit[] @relation("CommentAudits") author User? @relation(fields: [authorId], references: [id]) @@index([articleId, status, parentId]) @@index([authorId]) } model CommentAudit { id Int @id @default(autoincrement()) commentId Int status String // approved, rejected, suspicious reason String? // rule reason or AI decision score Float? // AI confidence score reviewerId Int? // null for AI reviewedAt DateTime @default(now()) comment Comment @relation("CommentAudits", fields: [commentId], references: [id]) reviewer User? @relation("CommentAuditReviewer", fields: [reviewerId], references: [id]) @@unique([commentId]) } model LinkAccess { id Int @id @default(autoincrement()) token String @unique articleId Int expiresAt DateTime? maxViews Int? viewCount Int @default(0) createdAt DateTime @default(now()) article Article @relation("ArticleLinkAccess", fields: [articleId], references: [id]) } model Reaction { id Int @id @default(autoincrement()) articleId Int userId Int type String // like, love, wow, etc. createdAt DateTime @default(now()) article Article @relation("ArticleReactions", fields: [articleId], references: [id]) user User @relation("UserReactions", fields: [userId], references: [id]) @@unique([userId, articleId, type]) } model Follow { id Int @id @default(autoincrement()) followerId Int followeeId Int createdAt DateTime @default(now()) follower User @relation("FollowFollower", fields: [followerId], references: [id]) followee User @relation("FollowFollowee", fields: [followeeId], references: [id]) @@unique([followerId, followeeId]) } model Subscription { id Int @id @default(autoincrement()) subscriberId Int targetId Int? type String // article, user, comment createdAt DateTime @default(now()) subscriber User @relation("SubscriptionSubscriber", fields: [subscriberId], references: [id]) target User? @relation("SubscriptionTarget", fields: [targetId], references: [id]) @@unique([subscriberId, targetId, type]) } model Notification { id Int @id @default(autoincrement()) userId Int type String // comment_reply, reaction, follow, system title String message String isRead Boolean @default(false) createdAt DateTime @default(now()) user User @relation("UserNotifications", fields: [userId], references: [id]) @@index([userId]) } model EmailMessage { id Int @id @default(autoincrement()) to String subject String body String status String @default("pending") // pending, sent, failed attempts Int @default(0) lastError String? scheduledAt DateTime @default(now()) sentAt DateTime? createdAt DateTime @default(now()) @@index([status, createdAt]) } model AnalyticsEvent { id String @id @default(cuid()) type String // page_view, article_view, comment_submit, etc. userId Int? sessionId String? ip String? userAgent String? data Json? // additional event data createdAt DateTime @default(now()) @@index([type, createdAt]) @@index([userId]) @@index([sessionId]) } model AnalyticsAggregate { id Int @id @default(autoincrement()) date DateTime // truncated to day type String // page_views, unique_visitors, etc. value Int metadata Json? @@unique([date, type]) }