Files
course-craft-service/packages/database/prisma/schema.prisma
root f39680d714 feat: AI-powered quiz generation with caching
- Add Quiz model to store generated questions in DB
- Integrate OpenRouter API to generate quiz from lesson content
- Cache quiz results to avoid regenerating on every request
- Extract text from TipTap JSON and send to AI
- Fallback to default questions if AI fails or no API key

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-06 10:58:15 +00:00

588 lines
18 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"
previewFeatures = ["postgresqlExtensions"]
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
extensions = [pgvector(map: "vector"), uuidOssp(map: "uuid-ossp")]
}
// ============================================
// User & Authentication
// ============================================
model User {
id String @id @default(uuid())
supabaseId String @unique @map("supabase_id")
email String @unique
name String?
avatarUrl String? @map("avatar_url")
// Subscription
subscriptionTier SubscriptionTier @default(FREE) @map("subscription_tier")
// Timestamps
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Role
role UserRole @default(USER)
// Relations
settings UserSettings?
subscription Subscription?
courses Course[] @relation("AuthoredCourses")
enrollments Enrollment[]
lessonProgress LessonProgress[]
purchases Purchase[]
reviews Review[]
generations CourseGeneration[]
groupMembers GroupMember[]
groupMessages GroupMessage[]
homeworkSubmissions HomeworkSubmission[]
supportTickets SupportTicket[]
ticketMessages TicketMessage[]
@@map("users")
}
model UserSettings {
id String @id @default(uuid())
userId String @unique @map("user_id")
// AI Settings - user can override default model
customAiModel String? @map("custom_ai_model") // e.g., "qwen/qwen3-coder-next"
// Notification settings
emailNotifications Boolean @default(true) @map("email_notifications")
marketingEmails Boolean @default(false) @map("marketing_emails")
// UI Preferences
theme String @default("system") // "light", "dark", "system"
language String @default("ru")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("user_settings")
}
enum UserRole {
USER
MODERATOR
ADMIN
}
// ============================================
// Subscription & Payments
// ============================================
enum SubscriptionTier {
FREE
PREMIUM
PRO
}
model Subscription {
id String @id @default(uuid())
userId String @unique @map("user_id")
tier SubscriptionTier @default(FREE)
// Stripe
stripeCustomerId String? @unique @map("stripe_customer_id")
stripeSubscriptionId String? @unique @map("stripe_subscription_id")
stripePriceId String? @map("stripe_price_id")
// Billing cycle
currentPeriodStart DateTime? @map("current_period_start")
currentPeriodEnd DateTime? @map("current_period_end")
cancelAtPeriodEnd Boolean @default(false) @map("cancel_at_period_end")
// Usage tracking
coursesCreatedThisMonth Int @default(0) @map("courses_created_this_month")
usageResetDate DateTime? @map("usage_reset_date")
// Status
status String @default("active") // active, canceled, past_due, incomplete
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("subscriptions")
}
// ============================================
// Courses
// ============================================
enum CourseStatus {
DRAFT
GENERATING
PENDING_REVIEW
PUBLISHED
REJECTED
ARCHIVED
}
model Course {
id String @id @default(uuid())
authorId String @map("author_id")
// Basic info
title String
description String? @db.Text
slug String @unique
coverImage String? @map("cover_image")
// Status
status CourseStatus @default(DRAFT)
// Marketplace
isPublished Boolean @default(false) @map("is_published")
price Decimal? @db.Decimal(10, 2) // null = free course
currency String @default("USD")
// Author verification — author checked the content and vouches for quality
isVerified Boolean @default(false) @map("is_verified")
// Categorization
categoryId String? @map("category_id")
tags String[] @default([])
difficulty String? // "beginner", "intermediate", "advanced"
estimatedHours Int? @map("estimated_hours")
// SEO & metadata
metaTitle String? @map("meta_title")
metaDescription String? @map("meta_description")
// Stats
viewCount Int @default(0) @map("view_count")
enrollmentCount Int @default(0) @map("enrollment_count")
averageRating Float? @map("average_rating")
// Timestamps
publishedAt DateTime? @map("published_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Moderation
moderationNote String? @db.Text @map("moderation_note")
moderatedAt DateTime? @map("moderated_at")
// Relations
author User @relation("AuthoredCourses", fields: [authorId], references: [id], onDelete: Cascade)
category Category? @relation(fields: [categoryId], references: [id])
chapters Chapter[]
enrollments Enrollment[]
purchases Purchase[]
reviews Review[]
generation CourseGeneration?
groups CourseGroup[]
// Vector embedding for semantic search
embedding Unsupported("vector(1536)")?
@@index([authorId])
@@index([status])
@@index([isPublished])
@@index([categoryId])
@@map("courses")
}
model Chapter {
id String @id @default(uuid())
courseId String @map("course_id")
title String
description String? @db.Text
order Int @default(0)
// Timestamps
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
lessons Lesson[]
@@index([courseId])
@@map("chapters")
}
model Lesson {
id String @id @default(uuid())
chapterId String @map("chapter_id")
title String
content Json? // TipTap JSON content
order Int @default(0)
// Duration estimate
durationMinutes Int? @map("duration_minutes")
// Media
videoUrl String? @map("video_url")
// Timestamps
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
chapter Chapter @relation(fields: [chapterId], references: [id], onDelete: Cascade)
homework Homework[]
quiz Quiz?
// Vector embedding for semantic search
embedding Unsupported("vector(1536)")?
@@index([chapterId])
@@map("lessons")
}
model Quiz {
id String @id @default(uuid())
lessonId String @unique @map("lesson_id")
questions Json // Array of {id, question, options, correctAnswer}
createdAt DateTime @default(now()) @map("created_at")
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
@@map("quizzes")
}
// ============================================
// AI Course Generation
// ============================================
enum GenerationStatus {
PENDING
ANALYZING
ASKING_QUESTIONS
WAITING_FOR_ANSWERS
RESEARCHING
GENERATING_OUTLINE
GENERATING_CONTENT
COMPLETED
FAILED
CANCELLED
}
model CourseGeneration {
id String @id @default(uuid())
userId String @map("user_id")
courseId String? @unique @map("course_id")
// Input
initialPrompt String @db.Text @map("initial_prompt")
// AI Configuration
aiModel String @map("ai_model")
// Status & Progress
status GenerationStatus @default(PENDING)
progress Int @default(0) // 0-100
currentStep String? @map("current_step")
// Clarifying questions flow
questions Json? // Array of questions to ask
answers Json? // User's answers
// Generated outline (intermediate state)
generatedOutline Json? @map("generated_outline")
// Error handling
errorMessage String? @db.Text @map("error_message")
retryCount Int @default(0) @map("retry_count")
// Job tracking
jobId String? @map("job_id") // BullMQ job ID
// Timestamps
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
course Course? @relation(fields: [courseId], references: [id], onDelete: SetNull)
@@index([userId])
@@index([status])
@@map("course_generations")
}
// ============================================
// Marketplace (Future)
// ============================================
model Category {
id String @id @default(uuid())
name String @unique
slug String @unique
description String? @db.Text
icon String?
parentId String? @map("parent_id")
order Int @default(0)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
parent Category? @relation("CategoryHierarchy", fields: [parentId], references: [id])
children Category[] @relation("CategoryHierarchy")
courses Course[]
@@map("categories")
}
model Purchase {
id String @id @default(uuid())
userId String @map("user_id")
courseId String @map("course_id")
// Payment details
amount Decimal @db.Decimal(10, 2)
currency String @default("USD")
stripePaymentId String? @map("stripe_payment_id")
// Status
status String @default("completed") // pending, completed, refunded
// Access
accessGrantedAt DateTime @default(now()) @map("access_granted_at")
accessExpiresAt DateTime? @map("access_expires_at") // null = lifetime access
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
@@unique([userId, courseId])
@@index([userId])
@@index([courseId])
@@map("purchases")
}
model Review {
id String @id @default(uuid())
userId String @map("user_id")
courseId String @map("course_id")
rating Int // 1-5
title String?
content String? @db.Text
// Moderation
isApproved Boolean @default(true) @map("is_approved")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
@@unique([userId, courseId])
@@index([courseId])
@@map("reviews")
}
// ============================================
// Enrollment & Progress
// ============================================
model Enrollment {
id String @id @default(uuid())
userId String @map("user_id")
courseId String @map("course_id")
// Progress
progress Int @default(0) // 0-100
completedAt DateTime? @map("completed_at")
// Certificate
certificateUrl String? @map("certificate_url")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
lessons LessonProgress[]
@@unique([userId, courseId])
@@index([userId])
@@index([courseId])
@@map("enrollments")
}
model LessonProgress {
id String @id @default(uuid())
userId String @map("user_id")
enrollmentId String @map("enrollment_id")
lessonId String @map("lesson_id")
completedAt DateTime? @map("completed_at")
quizScore Int? @map("quiz_score") // 0-100
createdAt DateTime @default(now()) @map("created_at")
// Relations
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
enrollment Enrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)
@@unique([userId, lessonId])
@@index([enrollmentId])
@@map("lesson_progress")
}
// ============================================
// Course Groups & Collaboration
// ============================================
model CourseGroup {
id String @id @default(uuid())
courseId String @map("course_id")
name String
description String? @db.Text
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
members GroupMember[]
messages GroupMessage[]
@@index([courseId])
@@map("course_groups")
}
model GroupMember {
id String @id @default(uuid())
groupId String @map("group_id")
userId String @map("user_id")
role String @default("student") // "teacher", "student"
joinedAt DateTime @default(now()) @map("joined_at")
group CourseGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([groupId, userId])
@@map("group_members")
}
model GroupMessage {
id String @id @default(uuid())
groupId String @map("group_id")
userId String @map("user_id")
content String @db.Text
createdAt DateTime @default(now()) @map("created_at")
group CourseGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([groupId])
@@map("group_messages")
}
// ============================================
// Homework & Assignments
// ============================================
model Homework {
id String @id @default(uuid())
lessonId String @map("lesson_id")
title String
description String @db.Text
dueDate DateTime? @map("due_date")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
submissions HomeworkSubmission[]
@@index([lessonId])
@@map("homework")
}
model HomeworkSubmission {
id String @id @default(uuid())
homeworkId String @map("homework_id")
userId String @map("user_id")
content String @db.Text
// AI grading
aiScore Int? @map("ai_score") // 0-100
aiFeedback String? @db.Text @map("ai_feedback")
// Teacher grading
teacherScore Int? @map("teacher_score") // 0-100
teacherFeedback String? @db.Text @map("teacher_feedback")
submittedAt DateTime @default(now()) @map("submitted_at")
gradedAt DateTime? @map("graded_at")
homework Homework @relation(fields: [homeworkId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([homeworkId, userId])
@@map("homework_submissions")
}
// ============================================
// Support Tickets
// ============================================
model SupportTicket {
id String @id @default(uuid())
userId String @map("user_id")
title String
status String @default("open") // "open", "in_progress", "resolved", "closed"
priority String @default("normal") // "low", "normal", "high"
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
messages TicketMessage[]
@@index([userId])
@@index([status])
@@map("support_tickets")
}
model TicketMessage {
id String @id @default(uuid())
ticketId String @map("ticket_id")
userId String @map("user_id")
content String @db.Text
isStaff Boolean @default(false) @map("is_staff")
createdAt DateTime @default(now()) @map("created_at")
ticket SupportTicket @relation(fields: [ticketId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([ticketId])
@@map("ticket_messages")
}