Import source: app/backend/prisma/schema.prisma
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,821 @@
|
|||||||
|
// Teknav DB schema — Postgres 16 + Prisma 5
|
||||||
|
// Mirrors the shapes used by the legacy localStorage TeknavStore.
|
||||||
|
// Persian-FTS extras (tsvector + GIN index + diacritic-stripping fn) live in
|
||||||
|
// prisma/migrations/00000000000001_persian_fts/migration.sql
|
||||||
|
// and are applied as a follow-up migration after the initial schema.
|
||||||
|
|
||||||
|
datasource db {
|
||||||
|
provider = "postgresql"
|
||||||
|
url = env("DATABASE_URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
generator client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UserRole {
|
||||||
|
admin
|
||||||
|
editor
|
||||||
|
writer
|
||||||
|
reviewer
|
||||||
|
reader
|
||||||
|
}
|
||||||
|
|
||||||
|
enum UserStatus {
|
||||||
|
active
|
||||||
|
suspended
|
||||||
|
pending
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NotificationType {
|
||||||
|
comment
|
||||||
|
comment_reply
|
||||||
|
new_article
|
||||||
|
review_approved
|
||||||
|
review_revision
|
||||||
|
review_rejected
|
||||||
|
review_submitted
|
||||||
|
system
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ReviewStatus {
|
||||||
|
pending
|
||||||
|
revision_requested
|
||||||
|
approved
|
||||||
|
rejected
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
phone String? @unique
|
||||||
|
username String? @unique // URL-safe slug, e.g. "coaxys"
|
||||||
|
passwordHash String?
|
||||||
|
name String
|
||||||
|
bio String? @db.VarChar(280)
|
||||||
|
avatarUrl String?
|
||||||
|
emailVerifiedAt DateTime?
|
||||||
|
phoneVerifiedAt DateTime?
|
||||||
|
twoFactorEnabled Boolean @default(false)
|
||||||
|
twoFactorMethod String? @default("sms")
|
||||||
|
membershipTier String @default("free")
|
||||||
|
membershipExpiresAt DateTime?
|
||||||
|
role UserRole @default(reader)
|
||||||
|
status UserStatus @default(active)
|
||||||
|
|
||||||
|
// Growth & Community
|
||||||
|
streakCount Int @default(0)
|
||||||
|
lastReadAt DateTime?
|
||||||
|
|
||||||
|
// OAuth — populated when the user signs in with Google/GitHub.
|
||||||
|
oauthProvider String? // 'google' | 'github'
|
||||||
|
oauthSubject String? // provider-side user id
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
lastLoginAt DateTime?
|
||||||
|
|
||||||
|
passwordChangedAt DateTime?
|
||||||
|
|
||||||
|
author Author?
|
||||||
|
corrections ArticleCorrection[] @relation("ArticleCorrectionEditor")
|
||||||
|
authoredArticles Article[] @relation("ArticleCreatedBy")
|
||||||
|
uploadedMedia MediaAsset[] @relation("MediaUploadedBy")
|
||||||
|
reactions Reaction[]
|
||||||
|
saved SavedArticle[]
|
||||||
|
activity ActivityLog[]
|
||||||
|
comments Comment[]
|
||||||
|
commentFlags CommentFlag[]
|
||||||
|
readHistory ReadHistory[]
|
||||||
|
notifications Notification[]
|
||||||
|
following WriterFollow[] @relation("Follower")
|
||||||
|
followers WriterFollow[] @relation("Writer")
|
||||||
|
topicFollows TopicFollow[]
|
||||||
|
newsletterCampaigns NewsletterCampaign[]
|
||||||
|
commentLikes CommentLike[]
|
||||||
|
commentUpvotes CommentUpvote[]
|
||||||
|
reviewAssignments ArticleReview[] @relation("ArticleReviewReviewer")
|
||||||
|
qualityReviews Article[] @relation("ArticleQualityReviewer")
|
||||||
|
readingLists ReadingList[]
|
||||||
|
analyticsEvents AnalyticsEvent[]
|
||||||
|
questions Question[]
|
||||||
|
answers Answer[]
|
||||||
|
clientErrors ClientError[]
|
||||||
|
membershipPayments MembershipPayment[]
|
||||||
|
pushSubscriptions PushSubscription[]
|
||||||
|
notificationPreferences NotificationPreference[]
|
||||||
|
badges UserBadge[]
|
||||||
|
revisionsAuthored ArticleRevision[] @relation("ArticleRevisionSavedBy")
|
||||||
|
wikiProposals WikiProposal[]
|
||||||
|
glossaryProposals GlossaryProposal[]
|
||||||
|
quizAttempts QuizAttempt[]
|
||||||
|
certifications UserCertification[]
|
||||||
|
companyClaims CompanyClaim[]
|
||||||
|
ownedCompany CompanyProfile? @relation("CompanyOwner")
|
||||||
|
glossaryReviewed GlossaryProposal[] @relation("GlossaryProposalReviewer")
|
||||||
|
wikiReviewed WikiProposal[] @relation("WikiProposalReviewer")
|
||||||
|
companyClaimsReviewed CompanyClaim[] @relation("CompanyClaimReviewer")
|
||||||
|
|
||||||
|
@@unique([oauthProvider, oauthSubject])
|
||||||
|
@@index([role, status])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Author {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
slug String @unique
|
||||||
|
name String
|
||||||
|
specialty String
|
||||||
|
bio String
|
||||||
|
initials String
|
||||||
|
color String
|
||||||
|
verifiedExpert Boolean @default(false)
|
||||||
|
verificationNote String?
|
||||||
|
social Json @default("{}")
|
||||||
|
articleCount Int @default(0)
|
||||||
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||||
|
userId String? @unique
|
||||||
|
articles Article[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Category {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
slug String @unique
|
||||||
|
name String
|
||||||
|
description String
|
||||||
|
color String
|
||||||
|
diagram String?
|
||||||
|
articleCount Int @default(0)
|
||||||
|
articles Article[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model Tag {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
name String @unique
|
||||||
|
count Int @default(0)
|
||||||
|
articles ArticleTag[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// type/status kept as String (not enum) to preserve the Persian values used by the
|
||||||
|
// legacy frontend without round-tripping through enum maps. Validation is enforced
|
||||||
|
// at the API layer via Zod.
|
||||||
|
model Article {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
slug String @unique
|
||||||
|
title String
|
||||||
|
subtitle String?
|
||||||
|
summary String
|
||||||
|
content String
|
||||||
|
type String
|
||||||
|
status String @default("پیشنویس")
|
||||||
|
readTime Int @default(5)
|
||||||
|
views Int @default(0)
|
||||||
|
reactions Int @default(0)
|
||||||
|
featured Boolean @default(false)
|
||||||
|
sponsored Boolean @default(false)
|
||||||
|
premiumOnly Boolean @default(false)
|
||||||
|
diagram String?
|
||||||
|
metaDescription String? @db.VarChar(300)
|
||||||
|
keywords String[] @default([])
|
||||||
|
ogTitle String? @db.VarChar(200)
|
||||||
|
ogDescription String? @db.VarChar(300)
|
||||||
|
ogImage String?
|
||||||
|
canonicalPath String?
|
||||||
|
dateModified DateTime?
|
||||||
|
factCheckedAt DateTime?
|
||||||
|
reviewedBy User? @relation("ArticleQualityReviewer", fields: [reviewedById], references: [id], onDelete: SetNull)
|
||||||
|
reviewedById String?
|
||||||
|
sourceNotes String?
|
||||||
|
contentFreshnessStatus String @default("current")
|
||||||
|
correctionNotice String?
|
||||||
|
|
||||||
|
category Category @relation(fields: [categoryId], references: [id])
|
||||||
|
categoryId String
|
||||||
|
|
||||||
|
author Author @relation(fields: [authorId], references: [id])
|
||||||
|
authorId String
|
||||||
|
|
||||||
|
createdBy User? @relation("ArticleCreatedBy", fields: [createdById], references: [id], onDelete: SetNull)
|
||||||
|
createdById String?
|
||||||
|
|
||||||
|
tags ArticleTag[]
|
||||||
|
reactionLog Reaction[]
|
||||||
|
savedBy SavedArticle[]
|
||||||
|
comments Comment[]
|
||||||
|
readHistory ReadHistory[]
|
||||||
|
viewLogs ArticleViewLog[]
|
||||||
|
seriesLinks SeriesArticle[]
|
||||||
|
review ArticleReview?
|
||||||
|
readingListItems ReadingListItem[]
|
||||||
|
analyticsEvents AnalyticsEvent[]
|
||||||
|
corrections ArticleCorrection[]
|
||||||
|
questions Question[]
|
||||||
|
revisions ArticleRevision[]
|
||||||
|
wikiProposals WikiProposal[]
|
||||||
|
articleModules ArticleModule[]
|
||||||
|
embeddings ArticleEmbedding?
|
||||||
|
search_tsv Unsupported("tsvector")?
|
||||||
|
|
||||||
|
publishedAt DateTime?
|
||||||
|
scheduledAt DateTime?
|
||||||
|
deletedAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([status, publishedAt(sort: Desc)])
|
||||||
|
@@index([status, scheduledAt])
|
||||||
|
@@index([categoryId, status])
|
||||||
|
@@index([authorId])
|
||||||
|
@@index([reviewedById])
|
||||||
|
@@index([featured, publishedAt(sort: Desc)])
|
||||||
|
@@index([sponsored, publishedAt(sort: Desc)])
|
||||||
|
@@index([premiumOnly, publishedAt(sort: Desc)])
|
||||||
|
@@index([publishedAt(sort: Desc)])
|
||||||
|
@@index([deletedAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model JobListing {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
company String
|
||||||
|
title String
|
||||||
|
location String
|
||||||
|
remote Boolean @default(true)
|
||||||
|
url String?
|
||||||
|
contactEmail String?
|
||||||
|
description String @db.VarChar(3000)
|
||||||
|
status String @default("pending")
|
||||||
|
featuredUntil DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([status, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Course {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
slug String @unique
|
||||||
|
title String
|
||||||
|
summary String
|
||||||
|
level String
|
||||||
|
price Int @default(0)
|
||||||
|
status String @default("published")
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([status, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ArticleCorrection {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String
|
||||||
|
note String @db.VarChar(1000)
|
||||||
|
editor User? @relation("ArticleCorrectionEditor", fields: [editorId], references: [id], onDelete: SetNull)
|
||||||
|
editorId String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([articleId, createdAt(sort: Desc)])
|
||||||
|
@@index([createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model MediaAsset {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
url String @unique
|
||||||
|
path String @unique
|
||||||
|
filename String
|
||||||
|
mimeType String
|
||||||
|
sizeBytes Int
|
||||||
|
width Int?
|
||||||
|
height Int?
|
||||||
|
uploadedBy User? @relation("MediaUploadedBy", fields: [uploadedById], references: [id], onDelete: SetNull)
|
||||||
|
uploadedById String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([createdAt(sort: Desc)])
|
||||||
|
@@index([mimeType])
|
||||||
|
@@index([uploadedById, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ArticleTag {
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String
|
||||||
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||||
|
tagId String
|
||||||
|
|
||||||
|
@@id([articleId, tagId])
|
||||||
|
@@index([tagId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Reaction {
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String
|
||||||
|
type String // insightful | curious | applause
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@id([userId, articleId, type])
|
||||||
|
@@index([articleId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model SavedArticle {
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String
|
||||||
|
savedAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@id([userId, articleId])
|
||||||
|
@@index([userId, savedAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Comment {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String
|
||||||
|
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
||||||
|
authorId String
|
||||||
|
replyTo Comment? @relation("CommentReplies", fields: [replyToId], references: [id], onDelete: Cascade)
|
||||||
|
replyToId String?
|
||||||
|
replies Comment[] @relation("CommentReplies")
|
||||||
|
body String @db.VarChar(1000)
|
||||||
|
flagCount Int @default(0)
|
||||||
|
likeCount Int @default(0)
|
||||||
|
upvoteCount Int @default(0)
|
||||||
|
flagged Boolean @default(false)
|
||||||
|
deletedAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
flags CommentFlag[]
|
||||||
|
likes CommentLike[]
|
||||||
|
upvotes CommentUpvote[]
|
||||||
|
|
||||||
|
@@index([articleId, upvoteCount(sort: Desc), createdAt(sort: Desc)])
|
||||||
|
@@index([authorId, createdAt(sort: Desc)])
|
||||||
|
@@index([flagged, createdAt(sort: Desc)])
|
||||||
|
@@index([deletedAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model CommentFlag {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
||||||
|
commentId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([commentId, userId])
|
||||||
|
@@index([userId, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model CommentLike {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
||||||
|
commentId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([commentId, userId])
|
||||||
|
@@index([userId, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model CommentUpvote {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
|
||||||
|
commentId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([commentId, userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ArticleViewLog {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String
|
||||||
|
date DateTime @db.Date
|
||||||
|
views Int @default(0)
|
||||||
|
|
||||||
|
@@unique([articleId, date])
|
||||||
|
@@index([date])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ReadHistory {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String
|
||||||
|
progress Float @default(0) // 0 to 1
|
||||||
|
readAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([userId, articleId])
|
||||||
|
@@index([userId, readAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Question {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String
|
||||||
|
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
||||||
|
authorId String
|
||||||
|
body String @db.VarChar(1000)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
answers Answer[]
|
||||||
|
|
||||||
|
@@index([articleId, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Answer {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
||||||
|
questionId String
|
||||||
|
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
|
||||||
|
authorId String
|
||||||
|
body String @db.VarChar(1000)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([questionId, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Notification {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
type NotificationType
|
||||||
|
payload Json
|
||||||
|
read Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId, read, createdAt(sort: Desc)])
|
||||||
|
@@index([createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model WriterFollow {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
follower User @relation("Follower", fields: [followerId], references: [id], onDelete: Cascade)
|
||||||
|
followerId String
|
||||||
|
writer User @relation("Writer", fields: [writerId], references: [id], onDelete: Cascade)
|
||||||
|
writerId String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([followerId, writerId])
|
||||||
|
@@index([writerId, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model TopicFollow {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
topic String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([userId, topic])
|
||||||
|
@@index([topic, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model NewsletterSubscriber {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
email String @unique
|
||||||
|
unsubscribeToken String @unique @default(cuid())
|
||||||
|
source String?
|
||||||
|
doubleOptInToken String? @unique
|
||||||
|
confirmedAt DateTime?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model AnalyticsEvent {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
type String
|
||||||
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||||
|
userId String?
|
||||||
|
article Article? @relation(fields: [articleId], references: [id], onDelete: SetNull)
|
||||||
|
articleId String?
|
||||||
|
topic String?
|
||||||
|
metadata Json @default("{}")
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([type, createdAt(sort: Desc)])
|
||||||
|
@@index([userId, createdAt(sort: Desc)])
|
||||||
|
@@index([articleId, createdAt(sort: Desc)])
|
||||||
|
@@index([topic, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model NewsletterCampaign {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
subject String @db.VarChar(200)
|
||||||
|
slug String? @unique
|
||||||
|
bodyHtml String
|
||||||
|
sentAt DateTime?
|
||||||
|
recipientCount Int @default(0)
|
||||||
|
createdBy User @relation(fields: [createdById], references: [id])
|
||||||
|
createdById String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([sentAt, createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ArticleSeries {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
title String @db.VarChar(200)
|
||||||
|
slug String @unique
|
||||||
|
description String? @db.VarChar(500)
|
||||||
|
coverImage String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
articles SeriesArticle[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model SeriesArticle {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
series ArticleSeries @relation(fields: [seriesId], references: [id], onDelete: Cascade)
|
||||||
|
seriesId String
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String
|
||||||
|
position Int
|
||||||
|
|
||||||
|
@@unique([seriesId, articleId])
|
||||||
|
@@unique([seriesId, position])
|
||||||
|
@@index([articleId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ArticleReview {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String @unique
|
||||||
|
reviewer User? @relation("ArticleReviewReviewer", fields: [reviewerId], references: [id])
|
||||||
|
reviewerId String?
|
||||||
|
status ReviewStatus @default(pending)
|
||||||
|
note String? @db.VarChar(1000)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model ReadingList {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
name String @db.VarChar(80)
|
||||||
|
isDefault Boolean @default(false)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
items ReadingListItem[]
|
||||||
|
|
||||||
|
@@unique([userId, name])
|
||||||
|
@@index([userId, createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ReadingListItem {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
list ReadingList @relation(fields: [listId], references: [id], onDelete: Cascade)
|
||||||
|
listId String
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String
|
||||||
|
position Int
|
||||||
|
addedAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([listId, articleId])
|
||||||
|
@@index([articleId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ActivityLog {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||||
|
userId String?
|
||||||
|
action String
|
||||||
|
target String
|
||||||
|
type String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([createdAt(sort: Desc)])
|
||||||
|
@@index([userId, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ClientError {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
message String @db.VarChar(1000)
|
||||||
|
stack String? @db.VarChar(5000)
|
||||||
|
url String? @db.VarChar(500)
|
||||||
|
userAgent String? @db.VarChar(500)
|
||||||
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||||
|
userId String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([createdAt(sort: Desc)])
|
||||||
|
@@index([message, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model MembershipPayment {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
authority String @unique
|
||||||
|
amount Int
|
||||||
|
currency String @default("IRR")
|
||||||
|
status String @default("pending")
|
||||||
|
refId String?
|
||||||
|
tier String @default("premium")
|
||||||
|
durationDays Int @default(365)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([userId, createdAt(sort: Desc)])
|
||||||
|
@@index([authority])
|
||||||
|
@@index([status, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
model PushSubscription {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String?
|
||||||
|
endpoint String @unique
|
||||||
|
auth String
|
||||||
|
p256dh String
|
||||||
|
userAgent String?
|
||||||
|
topics String[] @default([])
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model NotificationPreference {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
eventType String
|
||||||
|
channel String
|
||||||
|
enabled Boolean @default(true)
|
||||||
|
|
||||||
|
@@unique([userId, eventType, channel])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserBadge {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
badgeType String
|
||||||
|
earnedAt DateTime @default(now())
|
||||||
|
metadata Json @default("{}")
|
||||||
|
|
||||||
|
@@unique([userId, badgeType])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model ArticleRevision {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String
|
||||||
|
savedBy User? @relation("ArticleRevisionSavedBy", fields: [savedById], references: [id], onDelete: SetNull)
|
||||||
|
savedById String?
|
||||||
|
title String
|
||||||
|
content String
|
||||||
|
summary String
|
||||||
|
status String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([articleId, createdAt(sort: Desc)])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 14 Schema Extensions
|
||||||
|
|
||||||
|
// 1. Wiki / Glossary Proposals
|
||||||
|
model WikiProposal {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
articleId String
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
title String
|
||||||
|
content String
|
||||||
|
summary String
|
||||||
|
status ReviewStatus @default(pending)
|
||||||
|
rejectionReason String?
|
||||||
|
suggestedById String
|
||||||
|
suggestedBy User @relation(fields: [suggestedById], references: [id], onDelete: Cascade)
|
||||||
|
reviewedById String?
|
||||||
|
reviewedBy User? @relation("WikiProposalReviewer", fields: [reviewedById], references: [id], onDelete: SetNull)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@index([articleId])
|
||||||
|
}
|
||||||
|
|
||||||
|
model GlossaryProposal {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
term String
|
||||||
|
definition String
|
||||||
|
sameAs String[]
|
||||||
|
status ReviewStatus @default(pending)
|
||||||
|
rejectionReason String?
|
||||||
|
suggestedById String
|
||||||
|
suggestedBy User @relation(fields: [suggestedById], references: [id], onDelete: Cascade)
|
||||||
|
reviewedById String?
|
||||||
|
reviewedBy User? @relation("GlossaryProposalReviewer", fields: [reviewedById], references: [id], onDelete: SetNull)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Learning Paths & LMS
|
||||||
|
model LearningPath {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
slug String @unique
|
||||||
|
title String @db.VarChar(200)
|
||||||
|
description String
|
||||||
|
color String
|
||||||
|
modules LearningModule[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model LearningModule {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
pathId String
|
||||||
|
path LearningPath @relation(fields: [pathId], references: [id], onDelete: Cascade)
|
||||||
|
title String
|
||||||
|
order Int
|
||||||
|
articles ArticleModule[]
|
||||||
|
quizzes Quiz[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model ArticleModule {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
moduleId String
|
||||||
|
module LearningModule @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
order Int
|
||||||
|
}
|
||||||
|
|
||||||
|
model Quiz {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
moduleId String
|
||||||
|
module LearningModule @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
||||||
|
questions QuizQuestion[]
|
||||||
|
attempts QuizAttempt[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model QuizQuestion {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
quizId String
|
||||||
|
quiz Quiz @relation(fields: [quizId], references: [id], onDelete: Cascade)
|
||||||
|
questionText String
|
||||||
|
options String[]
|
||||||
|
correctOption Int
|
||||||
|
}
|
||||||
|
|
||||||
|
model QuizAttempt {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
quizId String
|
||||||
|
quiz Quiz @relation(fields: [quizId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
score Int
|
||||||
|
passed Boolean
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model UserCertification {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
pathId String
|
||||||
|
certificateUrl String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Farsi Tech & Startup Directory
|
||||||
|
model CompanyProfile {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
slug String @unique
|
||||||
|
name String
|
||||||
|
description String
|
||||||
|
logoUrl String?
|
||||||
|
websiteUrl String?
|
||||||
|
techStack String[]
|
||||||
|
verified Boolean @default(false)
|
||||||
|
claimedById String? @unique
|
||||||
|
claimedBy User? @relation("CompanyOwner", fields: [claimedById], references: [id], onDelete: SetNull)
|
||||||
|
claims CompanyClaim[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model CompanyClaim {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
companyId String
|
||||||
|
company CompanyProfile @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
documentsUrl String?
|
||||||
|
status ReviewStatus @default(pending)
|
||||||
|
reviewedById String?
|
||||||
|
reviewedBy User? @relation("CompanyClaimReviewer", fields: [reviewedById], references: [id], onDelete: SetNull)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Vector Embeddings
|
||||||
|
model ArticleEmbedding {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
article Article @relation(fields: [articleId], references: [id], onDelete: Cascade)
|
||||||
|
articleId String @unique
|
||||||
|
embedding Unsupported("vector(1536)")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user