350 lines
10 KiB
Plaintext
350 lines
10 KiB
Plaintext
// 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")
|
|
}
|
|
|
|
// ============================================================================
|
|
// ENUMS
|
|
// ============================================================================
|
|
|
|
enum UserRole {
|
|
USER
|
|
ADMIN
|
|
MODERATOR
|
|
}
|
|
|
|
enum CharacterDisplay {
|
|
SIMPLIFIED
|
|
TRADITIONAL
|
|
BOTH
|
|
}
|
|
|
|
enum Difficulty {
|
|
EASY
|
|
MEDIUM
|
|
HARD
|
|
SUSPENDED
|
|
}
|
|
|
|
// ============================================================================
|
|
// LANGUAGE & HANZI MODELS
|
|
// ============================================================================
|
|
|
|
model Language {
|
|
id String @id @default(cuid())
|
|
code String @unique // ISO 639-1 code (e.g., "en", "zh", "es")
|
|
name String // English name (e.g., "English", "Chinese")
|
|
nativeName String // Native name (e.g., "English", "中文")
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
meanings HanziMeaning[]
|
|
userPreferences UserPreference[]
|
|
|
|
@@map("languages")
|
|
}
|
|
|
|
model Hanzi {
|
|
id String @id @default(cuid())
|
|
simplified String @unique // The simplified Chinese character
|
|
radical String? // Radical/base component
|
|
frequency Int? // Frequency ranking (lower = more common)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
forms HanziForm[]
|
|
hskLevels HanziHSKLevel[]
|
|
partsOfSpeech HanziPOS[]
|
|
userProgress UserHanziProgress[]
|
|
collectionItems CollectionItem[]
|
|
sessionReviews SessionReview[]
|
|
|
|
@@index([simplified])
|
|
@@map("hanzi")
|
|
}
|
|
|
|
model HanziForm {
|
|
id String @id @default(cuid())
|
|
hanziId String
|
|
traditional String // Traditional variant
|
|
isDefault Boolean @default(false) // Primary traditional form
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
hanzi Hanzi @relation(fields: [hanziId], references: [id], onDelete: Cascade)
|
|
transcriptions HanziTranscription[]
|
|
meanings HanziMeaning[]
|
|
classifiers HanziClassifier[]
|
|
|
|
@@index([hanziId])
|
|
@@index([traditional])
|
|
@@map("hanzi_forms")
|
|
}
|
|
|
|
model HanziTranscription {
|
|
id String @id @default(cuid())
|
|
formId String
|
|
type String // "pinyin", "numeric", "wadegiles", etc.
|
|
value String // The actual transcription
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
form HanziForm @relation(fields: [formId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([formId])
|
|
@@index([type, value])
|
|
@@map("hanzi_transcriptions")
|
|
}
|
|
|
|
model HanziMeaning {
|
|
id String @id @default(cuid())
|
|
formId String
|
|
languageId String
|
|
meaning String @db.Text // Translation/definition
|
|
orderIndex Int @default(0) // Order of meanings
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
form HanziForm @relation(fields: [formId], references: [id], onDelete: Cascade)
|
|
language Language @relation(fields: [languageId], references: [id])
|
|
|
|
@@index([formId])
|
|
@@index([languageId])
|
|
@@map("hanzi_meanings")
|
|
}
|
|
|
|
model HanziHSKLevel {
|
|
id String @id @default(cuid())
|
|
hanziId String
|
|
level String // "new-1", "old-3", etc.
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
hanzi Hanzi @relation(fields: [hanziId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([hanziId])
|
|
@@index([level])
|
|
@@map("hanzi_hsk_levels")
|
|
}
|
|
|
|
model HanziPOS {
|
|
id String @id @default(cuid())
|
|
hanziId String
|
|
pos String // "n", "v", "adj", "adv", etc.
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
hanzi Hanzi @relation(fields: [hanziId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([hanziId])
|
|
@@map("hanzi_pos")
|
|
}
|
|
|
|
model HanziClassifier {
|
|
id String @id @default(cuid())
|
|
formId String
|
|
classifier String // Measure word
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
form HanziForm @relation(fields: [formId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([formId])
|
|
@@map("hanzi_classifiers")
|
|
}
|
|
|
|
// ============================================================================
|
|
// USER & AUTH MODELS
|
|
// ============================================================================
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
password String // Hashed with bcrypt
|
|
name String?
|
|
role UserRole @default(USER)
|
|
isActive Boolean @default(true)
|
|
emailVerified DateTime?
|
|
image String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
collections Collection[]
|
|
hanziProgress UserHanziProgress[]
|
|
preference UserPreference?
|
|
learningSessions LearningSession[]
|
|
accounts Account[]
|
|
sessions Session[]
|
|
|
|
@@index([email])
|
|
@@map("users")
|
|
}
|
|
|
|
model UserPreference {
|
|
id String @id @default(cuid())
|
|
userId String @unique
|
|
preferredLanguageId String
|
|
characterDisplay CharacterDisplay @default(SIMPLIFIED)
|
|
transcriptionType String @default("pinyin") // "pinyin", "numeric", etc.
|
|
cardsPerSession Int @default(20)
|
|
dailyGoal Int @default(50) // Cards per day
|
|
removalThreshold Int @default(10) // Consecutive correct before suggesting removal
|
|
allowManualDifficulty Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
preferredLanguage Language @relation(fields: [preferredLanguageId], references: [id])
|
|
|
|
@@map("user_preferences")
|
|
}
|
|
|
|
// NextAuth.js models
|
|
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])
|
|
@@map("accounts")
|
|
}
|
|
|
|
model Session {
|
|
id String @id @default(cuid())
|
|
sessionToken String @unique
|
|
userId String
|
|
expires DateTime
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("sessions")
|
|
}
|
|
|
|
model VerificationToken {
|
|
identifier String
|
|
token String @unique
|
|
expires DateTime
|
|
|
|
@@unique([identifier, token])
|
|
@@map("verification_tokens")
|
|
}
|
|
|
|
// ============================================================================
|
|
// LEARNING MODELS
|
|
// ============================================================================
|
|
|
|
model Collection {
|
|
id String @id @default(cuid())
|
|
name String
|
|
description String? @db.Text
|
|
isGlobal Boolean @default(false) // Global collections (HSK levels) vs user collections
|
|
createdBy String?
|
|
isPublic Boolean @default(false) // User collections can be public
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
creator User? @relation(fields: [createdBy], references: [id], onDelete: SetNull)
|
|
items CollectionItem[]
|
|
learningSessions LearningSession[]
|
|
|
|
@@index([createdBy])
|
|
@@index([isGlobal])
|
|
@@map("collections")
|
|
}
|
|
|
|
model CollectionItem {
|
|
id String @id @default(cuid())
|
|
collectionId String
|
|
hanziId String
|
|
orderIndex Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
collection Collection @relation(fields: [collectionId], references: [id], onDelete: Cascade)
|
|
hanzi Hanzi @relation(fields: [hanziId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([collectionId, hanziId])
|
|
@@index([collectionId])
|
|
@@index([hanziId])
|
|
@@map("collection_items")
|
|
}
|
|
|
|
model UserHanziProgress {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
hanziId String
|
|
correctCount Int @default(0)
|
|
incorrectCount Int @default(0)
|
|
consecutiveCorrect Int @default(0)
|
|
easeFactor Float @default(2.5) // SM-2 algorithm
|
|
interval Int @default(1) // Days between reviews
|
|
nextReviewDate DateTime @default(now())
|
|
manualDifficulty Difficulty? // Optional manual override
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
hanzi Hanzi @relation(fields: [hanziId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, hanziId])
|
|
@@index([userId, nextReviewDate])
|
|
@@index([hanziId])
|
|
@@map("user_hanzi_progress")
|
|
}
|
|
|
|
model LearningSession {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
collectionId String?
|
|
startedAt DateTime @default(now())
|
|
endedAt DateTime?
|
|
cardsReviewed Int @default(0)
|
|
correctAnswers Int @default(0)
|
|
incorrectAnswers Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
collection Collection? @relation(fields: [collectionId], references: [id], onDelete: SetNull)
|
|
reviews SessionReview[]
|
|
|
|
@@index([userId])
|
|
@@index([startedAt])
|
|
@@map("learning_sessions")
|
|
}
|
|
|
|
model SessionReview {
|
|
id String @id @default(cuid())
|
|
sessionId String
|
|
hanziId String
|
|
isCorrect Boolean
|
|
responseTime Int? // Milliseconds
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
session LearningSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
|
hanzi Hanzi @relation(fields: [hanziId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([sessionId])
|
|
@@index([hanziId])
|
|
@@map("session_reviews")
|
|
}
|