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