Files
memohanzi/prisma/schema.prisma
2025-11-21 09:51:16 +01:00

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")
}