Files
course-craft-service/packages/database/prisma/schema.prisma
root 2ed65f5678 feat: add course catalog, enrollment, progress tracking, quizzes, and reviews
Backend changes:
- Add Enrollment and LessonProgress models to track user progress
- Add UserRole enum (USER, MODERATOR, ADMIN)
- Add course verification and moderation fields
- New CatalogModule: public course browsing, publishing, verification
- New EnrollmentModule: enroll, progress tracking, quiz submission, reviews
- Add quiz generation endpoint to LessonsController

Frontend changes:
- Redesign course viewer: proper course UI with lesson navigation, progress bar
- Add beautiful typography styles for course content (prose-course)
- Fix first-login bug with token exchange retry logic
- New pages: /catalog (public courses), /catalog/[id] (course details), /learning (enrollments)
- Add LessonQuiz component with scoring and results
- Update sidebar navigation: add Catalog and My Learning links
- Add publish/verify buttons in course editor
- Integrate enrollment progress tracking with backend

All courses now support: sequential progression, quiz tests, reviews, ratings,
author verification badges, and full marketplace publishing workflow.

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

434 lines
13 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[]
@@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?
// 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)
// Vector embedding for semantic search
embedding Unsupported("vector(1536)")?
@@index([chapterId])
@@map("lessons")
}
// ============================================
// 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")
}