Fix production TypeScript builds and add DEPLOY.md so the app can be hosted from this repo. Co-authored-by: Cursor <cursoragent@cursor.com>
1045 lines
39 KiB
Plaintext
1045 lines
39 KiB
Plaintext
// DESIGN ARTIFACT — not the runtime backend schema.
|
|
// Active backend schema: backend/prisma/schema.prisma
|
|
// Do not run migrations from this file against the backend MySQL database.
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "mysql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
enum UserStatus {
|
|
active
|
|
pending_verification
|
|
suspended
|
|
deleted
|
|
}
|
|
|
|
enum AuthProvider {
|
|
password
|
|
google
|
|
phone_otp
|
|
}
|
|
|
|
enum SessionStatus {
|
|
active
|
|
revoked
|
|
expired
|
|
}
|
|
|
|
enum EnrollmentStatus {
|
|
active
|
|
paused
|
|
completed
|
|
dropped
|
|
}
|
|
|
|
enum UnlockReason {
|
|
schedule
|
|
manual
|
|
prerequisite_completed
|
|
retry
|
|
}
|
|
|
|
enum LessonProgressStatus {
|
|
not_started
|
|
in_progress
|
|
completed
|
|
skipped
|
|
}
|
|
|
|
enum SubmissionStatus {
|
|
draft
|
|
submitted
|
|
reviewed
|
|
}
|
|
|
|
enum EventSource {
|
|
user
|
|
system
|
|
ai
|
|
admin
|
|
}
|
|
|
|
enum AttemptPhase {
|
|
pre
|
|
post
|
|
}
|
|
|
|
enum AttemptStatus {
|
|
in_progress
|
|
submitted
|
|
scored
|
|
canceled
|
|
}
|
|
|
|
enum AiRunStatus {
|
|
queued
|
|
running
|
|
succeeded
|
|
failed
|
|
canceled
|
|
}
|
|
|
|
enum AiRunType {
|
|
assessment_analysis
|
|
final_report
|
|
exercise_feedback
|
|
lesson_chat
|
|
}
|
|
|
|
enum ChatRole {
|
|
system
|
|
user
|
|
assistant
|
|
tool
|
|
}
|
|
|
|
enum RoleCode {
|
|
admin
|
|
researcher
|
|
user
|
|
}
|
|
|
|
enum JourneyStep {
|
|
registered
|
|
pre_assessment_assigned
|
|
pre_assessment_completed
|
|
in_program
|
|
lesson_in_progress
|
|
exercise_pending
|
|
post_assessment_assigned
|
|
post_assessment_completed
|
|
completed
|
|
}
|
|
|
|
enum DailyCheckinSource {
|
|
manual
|
|
ai
|
|
system
|
|
}
|
|
|
|
enum AssessmentAssignmentStatus {
|
|
assigned
|
|
available
|
|
started
|
|
completed
|
|
expired
|
|
canceled
|
|
}
|
|
|
|
enum NotificationQueueStatus {
|
|
pending
|
|
sent
|
|
failed
|
|
canceled
|
|
}
|
|
|
|
model Organization {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
externalId String @unique @default(uuid()) @db.Char(36)
|
|
name String @db.VarChar(191)
|
|
slug String @unique @db.VarChar(191)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
users User[]
|
|
programs Program[]
|
|
providers AiProvider[]
|
|
}
|
|
|
|
model User {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
externalId String @unique @default(uuid()) @db.Char(36)
|
|
organizationId BigInt? @db.UnsignedBigInt
|
|
email String? @unique(map: "uq_users_email") @db.VarChar(191)
|
|
phone String? @unique(map: "uq_users_phone") @db.VarChar(32)
|
|
status UserStatus @default(active)
|
|
locale String @default("fa-IR") @db.VarChar(16)
|
|
timezone String @default("Asia/Tehran") @db.VarChar(64)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
deletedAt DateTime?
|
|
|
|
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
|
|
profile UserProfile?
|
|
authIdentities AuthIdentity[]
|
|
authSessions AuthSession[]
|
|
userRoles UserRole[]
|
|
userConsents UserConsent[]
|
|
assessmentAssignments AssessmentAssignment[]
|
|
attempts AssessmentAttempt[]
|
|
enrollments ProgramEnrollment[]
|
|
journeyState UserJourneyState?
|
|
dailyCheckins DailyCheckin[]
|
|
journeyEvents UserJourneyEvent[]
|
|
dailySummaries DailyActivitySummary[]
|
|
aiRuns AiAnalysisRun[]
|
|
aiMemories AiMemory[]
|
|
chatSessions ChatSession[]
|
|
chatMessages ChatMessage[]
|
|
sessionSummaries SessionSummary[]
|
|
feedbacks AiFeedback[]
|
|
notificationQueue NotificationQueue[]
|
|
behaviorLogs BehaviorLog[]
|
|
auditLogs AuditLog[]
|
|
assessmentComparisons AssessmentComparison[]
|
|
|
|
@@index([organizationId], map: "idx_users_organization_id")
|
|
@@index([createdAt], map: "idx_users_created_at")
|
|
}
|
|
|
|
model AuthIdentity {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
provider AuthProvider
|
|
providerUserId String @db.VarChar(191)
|
|
providerEmail String? @db.VarChar(191)
|
|
metadata Json?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([provider, providerUserId], map: "uq_auth_identities_provider_provider_user_id")
|
|
@@index([userId], map: "idx_auth_identities_user_id")
|
|
}
|
|
|
|
model AuthSession {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
refreshTokenHash String @unique @db.VarChar(255)
|
|
status SessionStatus @default(active)
|
|
ipAddress String? @db.VarChar(45)
|
|
userAgent String? @db.VarChar(512)
|
|
expiresAt DateTime
|
|
revokedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId, status], map: "idx_auth_sessions_user_id_status")
|
|
@@index([expiresAt], map: "idx_auth_sessions_expires_at")
|
|
}
|
|
|
|
model Role {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
code RoleCode @unique
|
|
displayName String @db.VarChar(128)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
userRoles UserRole[]
|
|
}
|
|
|
|
model UserRole {
|
|
userId BigInt @db.UnsignedBigInt
|
|
roleId BigInt @db.UnsignedBigInt
|
|
assignedAt DateTime @default(now())
|
|
assignedById BigInt? @db.UnsignedBigInt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([userId, roleId])
|
|
@@index([roleId], map: "idx_user_roles_role_id")
|
|
}
|
|
|
|
model UserProfile {
|
|
userId BigInt @id @db.UnsignedBigInt
|
|
firstName String? @db.VarChar(100)
|
|
lastName String? @db.VarChar(100)
|
|
displayName String? @db.VarChar(191)
|
|
avatarUrl String? @db.VarChar(512)
|
|
birthDate DateTime?
|
|
gender String? @db.VarChar(32)
|
|
countryCode String? @db.VarChar(8)
|
|
city String? @db.VarChar(100)
|
|
educationLevel String? @db.VarChar(100)
|
|
occupation String? @db.VarChar(100)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model Consent {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
key String @db.VarChar(100)
|
|
policyVersion String @db.VarChar(32)
|
|
title String @db.VarChar(191)
|
|
body String @db.Text
|
|
required Boolean @default(true)
|
|
active Boolean @default(true)
|
|
publishedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
userConsents UserConsent[]
|
|
|
|
@@unique([key, policyVersion], map: "uq_consents_key_policy_version")
|
|
}
|
|
|
|
model UserConsent {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
consentId BigInt @db.UnsignedBigInt
|
|
accepted Boolean @default(true)
|
|
acceptedAt DateTime @default(now())
|
|
ipAddress String? @db.VarChar(45)
|
|
userAgent String? @db.VarChar(512)
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
consent Consent @relation(fields: [consentId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, consentId], map: "uq_user_consents_user_id_consent_id")
|
|
@@index([consentId], map: "idx_user_consents_consent_id")
|
|
}
|
|
|
|
model AssessmentScale {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
key String @unique @db.VarChar(64)
|
|
title String @db.VarChar(191)
|
|
description String? @db.Text
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
versions AssessmentScaleVersion[]
|
|
}
|
|
|
|
model AssessmentScaleVersion {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
scaleId BigInt @db.UnsignedBigInt
|
|
version String @db.VarChar(32)
|
|
isActive Boolean @default(false)
|
|
scoringMethod String @db.VarChar(64)
|
|
metadata Json?
|
|
effectiveFrom DateTime?
|
|
effectiveTo DateTime?
|
|
createdAt DateTime @default(now())
|
|
|
|
scale AssessmentScale @relation(fields: [scaleId], references: [id], onDelete: Cascade)
|
|
questions AssessmentQuestion[]
|
|
attempts AssessmentAttempt[]
|
|
assignments AssessmentAssignment[]
|
|
|
|
@@unique([scaleId, version], map: "uq_assessment_scale_versions_scale_id_version")
|
|
@@index([scaleId, isActive], map: "idx_assessment_scale_versions_scale_id_is_active")
|
|
}
|
|
|
|
model AssessmentQuestion {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
scaleVersionId BigInt @db.UnsignedBigInt
|
|
code String @db.VarChar(64)
|
|
orderNo Int
|
|
prompt String @db.Text
|
|
reverseScored Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
scaleVersion AssessmentScaleVersion @relation(fields: [scaleVersionId], references: [id], onDelete: Cascade)
|
|
options AssessmentOption[]
|
|
answers AssessmentAnswer[]
|
|
|
|
@@unique([scaleVersionId, code], map: "uq_assessment_questions_scale_version_id_code")
|
|
@@unique([scaleVersionId, orderNo], map: "uq_assessment_questions_scale_version_id_order_no")
|
|
}
|
|
|
|
model AssessmentOption {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
questionId BigInt @db.UnsignedBigInt
|
|
value Int
|
|
label String @db.VarChar(191)
|
|
points Int
|
|
orderNo Int
|
|
|
|
question AssessmentQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([questionId, value], map: "uq_assessment_options_question_id_value")
|
|
@@unique([questionId, orderNo], map: "uq_assessment_options_question_id_order_no")
|
|
}
|
|
|
|
model AssessmentAttempt {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
scaleVersionId BigInt @db.UnsignedBigInt
|
|
enrollmentId BigInt? @db.UnsignedBigInt
|
|
assignmentId BigInt? @db.UnsignedBigInt
|
|
phase AttemptPhase
|
|
status AttemptStatus @default(in_progress)
|
|
startedAt DateTime @default(now())
|
|
submittedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
scaleVersion AssessmentScaleVersion @relation(fields: [scaleVersionId], references: [id], onDelete: Restrict)
|
|
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
|
|
assignment AssessmentAssignment? @relation(fields: [assignmentId], references: [id], onDelete: SetNull)
|
|
answers AssessmentAnswer[]
|
|
scores AssessmentScore[]
|
|
comparisonsAsPre AssessmentComparison[] @relation("ComparisonPreAttempt")
|
|
comparisonsAsPost AssessmentComparison[] @relation("ComparisonPostAttempt")
|
|
|
|
@@index([userId, phase, createdAt], map: "idx_assessment_attempts_user_id_phase_created_at")
|
|
@@index([enrollmentId], map: "idx_assessment_attempts_enrollment_id")
|
|
@@index([assignmentId], map: "idx_assessment_attempts_assignment_id")
|
|
}
|
|
|
|
model AssessmentAssignment {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
enrollmentId BigInt? @db.UnsignedBigInt
|
|
scaleVersionId BigInt @db.UnsignedBigInt
|
|
phase AttemptPhase
|
|
status AssessmentAssignmentStatus @default(assigned)
|
|
eligibleFrom DateTime?
|
|
dueAt DateTime?
|
|
attemptLimit Int?
|
|
attemptsUsed Int @default(0)
|
|
completedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
|
|
scaleVersion AssessmentScaleVersion @relation(fields: [scaleVersionId], references: [id], onDelete: Restrict)
|
|
attempts AssessmentAttempt[]
|
|
|
|
@@index([userId, phase, status], map: "idx_assessment_assignments_user_phase_status")
|
|
@@index([enrollmentId], map: "idx_assessment_assignments_enrollment_id")
|
|
@@index([dueAt], map: "idx_assessment_assignments_due_at")
|
|
@@map("assessment_assignments")
|
|
}
|
|
|
|
model AssessmentAnswer {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
attemptId BigInt @db.UnsignedBigInt
|
|
questionId BigInt @db.UnsignedBigInt
|
|
selectedValue Int?
|
|
answerText String? @db.Text
|
|
points Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
attempt AssessmentAttempt @relation(fields: [attemptId], references: [id], onDelete: Cascade)
|
|
question AssessmentQuestion @relation(fields: [questionId], references: [id], onDelete: Restrict)
|
|
|
|
@@unique([attemptId, questionId], map: "uq_assessment_answers_attempt_id_question_id")
|
|
}
|
|
|
|
model AssessmentScore {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
attemptId BigInt @db.UnsignedBigInt
|
|
metricKey String @db.VarChar(100)
|
|
rawScore Decimal @db.Decimal(10, 2)
|
|
normalizedScore Decimal? @db.Decimal(10, 2)
|
|
interpretation String? @db.Text
|
|
createdAt DateTime @default(now())
|
|
|
|
attempt AssessmentAttempt @relation(fields: [attemptId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([attemptId, metricKey], map: "uq_assessment_scores_attempt_id_metric_key")
|
|
}
|
|
|
|
model AssessmentComparison {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
enrollmentId BigInt? @db.UnsignedBigInt
|
|
preAttemptId BigInt @unique
|
|
postAttemptId BigInt @unique
|
|
summaryText String? @db.Text
|
|
deltaJson Json?
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
|
|
preAttempt AssessmentAttempt @relation("ComparisonPreAttempt", fields: [preAttemptId], references: [id], onDelete: Restrict)
|
|
postAttempt AssessmentAttempt @relation("ComparisonPostAttempt", fields: [postAttemptId], references: [id], onDelete: Restrict)
|
|
|
|
@@index([userId, createdAt], map: "idx_assessment_comparisons_user_id_created_at")
|
|
}
|
|
|
|
model Program {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
organizationId BigInt? @db.UnsignedBigInt
|
|
key String @unique @db.VarChar(64)
|
|
title String @db.VarChar(191)
|
|
description String? @db.Text
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
|
|
versions ProgramVersion[]
|
|
}
|
|
|
|
model ProgramVersion {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
programId BigInt @db.UnsignedBigInt
|
|
version String @db.VarChar(32)
|
|
isActive Boolean @default(false)
|
|
totalDays Int @default(0)
|
|
metadata Json?
|
|
createdAt DateTime @default(now())
|
|
|
|
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
|
modules Module[]
|
|
enrollments ProgramEnrollment[]
|
|
chatSessions ChatSession[]
|
|
journeyStates UserJourneyState[]
|
|
|
|
@@unique([programId, version], map: "uq_program_versions_program_id_version")
|
|
}
|
|
|
|
model Module {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
programVersionId BigInt @db.UnsignedBigInt
|
|
code String @db.VarChar(64)
|
|
title String @db.VarChar(191)
|
|
summary String? @db.Text
|
|
sequenceNo Int
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
programVersion ProgramVersion @relation(fields: [programVersionId], references: [id], onDelete: Cascade)
|
|
lessons Lesson[]
|
|
journeyStates UserJourneyState[]
|
|
chatSessions ChatSession[]
|
|
|
|
@@unique([programVersionId, code], map: "uq_modules_program_version_id_code")
|
|
@@unique([programVersionId, sequenceNo], map: "uq_modules_program_version_id_sequence_no")
|
|
@@unique([id, programVersionId], map: "uq_modules_id_program_version_id")
|
|
@@map("modules")
|
|
}
|
|
|
|
model Lesson {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
moduleId BigInt @db.UnsignedBigInt
|
|
code String @db.VarChar(64)
|
|
title String @db.VarChar(191)
|
|
summary String? @db.Text
|
|
dayNumber Int
|
|
sequenceNo Int
|
|
unlockRuleType String @db.VarChar(64)
|
|
estimatedMinutes Int?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
module Module @relation(fields: [moduleId], references: [id], onDelete: Cascade)
|
|
contents LessonContent[]
|
|
exercises Exercise[]
|
|
unlocks UserLessonUnlock[]
|
|
progresses UserLessonProgress[]
|
|
chatSessions ChatSession[]
|
|
aiMemories AiMemory[]
|
|
journeyStates UserJourneyState[]
|
|
sessionSummaries SessionSummary[]
|
|
|
|
@@unique([moduleId, code], map: "uq_lessons_module_id_code")
|
|
@@unique([moduleId, sequenceNo], map: "uq_lessons_module_id_sequence_no")
|
|
@@unique([id, moduleId], map: "uq_lessons_id_module_id")
|
|
}
|
|
|
|
model LessonContent {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
lessonId BigInt @db.UnsignedBigInt
|
|
contentType String @db.VarChar(32)
|
|
title String? @db.VarChar(191)
|
|
body String? @db.LongText
|
|
mediaUrl String? @db.VarChar(512)
|
|
orderNo Int
|
|
metadata Json?
|
|
createdAt DateTime @default(now())
|
|
|
|
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([lessonId, orderNo], map: "uq_lesson_contents_lesson_id_order_no")
|
|
}
|
|
|
|
model Exercise {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
lessonId BigInt @db.UnsignedBigInt
|
|
code String @db.VarChar(64)
|
|
title String @db.VarChar(191)
|
|
instructions String? @db.Text
|
|
required Boolean @default(true)
|
|
orderNo Int
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
|
prompts ExercisePrompt[]
|
|
submissions UserExerciseSubmission[]
|
|
|
|
@@unique([lessonId, code], map: "uq_exercises_lesson_id_code")
|
|
@@unique([lessonId, orderNo], map: "uq_exercises_lesson_id_order_no")
|
|
}
|
|
|
|
model ExercisePrompt {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
exerciseId BigInt @db.UnsignedBigInt
|
|
code String @db.VarChar(64)
|
|
promptText String @db.Text
|
|
inputType String @db.VarChar(32)
|
|
orderNo Int
|
|
required Boolean @default(true)
|
|
|
|
exercise Exercise @relation(fields: [exerciseId], references: [id], onDelete: Cascade)
|
|
answers UserExerciseAnswer[]
|
|
|
|
@@unique([exerciseId, code], map: "uq_exercise_prompts_exercise_id_code")
|
|
@@unique([exerciseId, orderNo], map: "uq_exercise_prompts_exercise_id_order_no")
|
|
}
|
|
|
|
model ProgramEnrollment {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
programVersionId BigInt @db.UnsignedBigInt
|
|
status EnrollmentStatus @default(active)
|
|
startedAt DateTime @default(now())
|
|
endedAt DateTime?
|
|
currentDay Int @default(0)
|
|
metadata Json?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
programVersion ProgramVersion @relation(fields: [programVersionId], references: [id], onDelete: Restrict)
|
|
attempts AssessmentAttempt[]
|
|
assessmentAssignments AssessmentAssignment[]
|
|
comparisons AssessmentComparison[]
|
|
lessonUnlocks UserLessonUnlock[]
|
|
lessonProgresses UserLessonProgress[]
|
|
exerciseSubmissions UserExerciseSubmission[]
|
|
chatSessions ChatSession[]
|
|
aiRuns AiAnalysisRun[]
|
|
journeyEvents UserJourneyEvent[]
|
|
journeyStates UserJourneyState[]
|
|
dailyCheckins DailyCheckin[]
|
|
aiMemories AiMemory[]
|
|
sessionSummaries SessionSummary[]
|
|
notificationQueue NotificationQueue[]
|
|
|
|
@@unique([userId, programVersionId, startedAt], map: "uq_user_enrollments_user_program_started")
|
|
@@unique([id, userId, programVersionId], map: "uq_program_enrollments_id_user_program_version_id")
|
|
@@index([userId, status], map: "idx_user_program_enrollments_user_id_status")
|
|
@@map("program_enrollments")
|
|
}
|
|
|
|
model UserJourneyState {
|
|
userId BigInt @id @db.UnsignedBigInt
|
|
enrollmentId BigInt? @db.UnsignedBigInt
|
|
currentStep JourneyStep @default(registered)
|
|
currentProgramVersionId BigInt? @db.UnsignedBigInt
|
|
currentModuleId BigInt? @db.UnsignedBigInt
|
|
currentLessonId BigInt? @db.UnsignedBigInt
|
|
nextUnlockAt DateTime?
|
|
finishedAt DateTime?
|
|
lastActivityAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
|
|
currentProgramVersion ProgramVersion? @relation(fields: [currentProgramVersionId], references: [id], onDelete: SetNull)
|
|
currentModule Module? @relation(fields: [currentModuleId], references: [id], onDelete: SetNull)
|
|
currentLesson Lesson? @relation(fields: [currentLessonId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([enrollmentId], map: "idx_user_journey_state_enrollment_id")
|
|
@@index([currentStep, nextUnlockAt], map: "idx_user_journey_state_step_next_unlock")
|
|
@@map("user_journey_state")
|
|
}
|
|
|
|
model DailyCheckin {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
enrollmentId BigInt? @db.UnsignedBigInt
|
|
checkinDate DateTime @db.Date
|
|
mood Int
|
|
energy Int
|
|
stress Int
|
|
sleep Int
|
|
note String? @db.Text
|
|
source DailyCheckinSource @default(manual)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
|
|
|
|
@@unique([userId, checkinDate], map: "uq_daily_checkins_user_id_checkin_date")
|
|
@@index([enrollmentId], map: "idx_daily_checkins_enrollment_id")
|
|
@@map("daily_checkins")
|
|
}
|
|
|
|
model UserLessonUnlock {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
enrollmentId BigInt @db.UnsignedBigInt
|
|
lessonId BigInt @db.UnsignedBigInt
|
|
reason UnlockReason
|
|
scheduledAt DateTime?
|
|
unlockedAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
|
|
enrollment ProgramEnrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)
|
|
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([enrollmentId, lessonId], map: "uq_user_lesson_unlocks_enrollment_id_lesson_id")
|
|
@@index([lessonId], map: "idx_user_lesson_unlocks_lesson_id")
|
|
}
|
|
|
|
model UserLessonProgress {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
enrollmentId BigInt @db.UnsignedBigInt
|
|
lessonId BigInt @db.UnsignedBigInt
|
|
status LessonProgressStatus @default(not_started)
|
|
startedAt DateTime?
|
|
completedAt DateTime?
|
|
progressPercent Decimal @default(0) @db.Decimal(5, 2)
|
|
updatedAt DateTime @updatedAt
|
|
createdAt DateTime @default(now())
|
|
|
|
enrollment ProgramEnrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)
|
|
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
|
|
chatSessions ChatSession[]
|
|
feedbacks AiFeedback[]
|
|
|
|
@@unique([enrollmentId, lessonId], map: "uq_user_lesson_progress_enrollment_id_lesson_id")
|
|
@@index([status, updatedAt], map: "idx_user_lesson_progress_status_updated_at")
|
|
}
|
|
|
|
model UserExerciseSubmission {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
enrollmentId BigInt @db.UnsignedBigInt
|
|
exerciseId BigInt @db.UnsignedBigInt
|
|
attemptNo Int @default(1)
|
|
status SubmissionStatus @default(draft)
|
|
submittedAt DateTime?
|
|
score Decimal? @db.Decimal(10, 2)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
enrollment ProgramEnrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)
|
|
exercise Exercise @relation(fields: [exerciseId], references: [id], onDelete: Restrict)
|
|
answers UserExerciseAnswer[]
|
|
feedbacks AiFeedback[]
|
|
|
|
@@unique([enrollmentId, exerciseId, attemptNo], map: "uq_exercise_submissions_enrollment_exercise_attempt")
|
|
@@index([status, submittedAt], map: "idx_user_exercise_submissions_status_submitted_at")
|
|
}
|
|
|
|
model UserExerciseAnswer {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
submissionId BigInt @db.UnsignedBigInt
|
|
promptId BigInt @db.UnsignedBigInt
|
|
answerText String? @db.LongText
|
|
answerJson Json?
|
|
createdAt DateTime @default(now())
|
|
|
|
submission UserExerciseSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
|
|
prompt ExercisePrompt @relation(fields: [promptId], references: [id], onDelete: Restrict)
|
|
|
|
@@unique([submissionId, promptId], map: "uq_user_exercise_answers_submission_id_prompt_id")
|
|
}
|
|
|
|
model UserJourneyEvent {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
enrollmentId BigInt? @db.UnsignedBigInt
|
|
eventName String @db.VarChar(100)
|
|
source EventSource @default(system)
|
|
eventAt DateTime @default(now())
|
|
payload Json?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId, eventAt], map: "idx_user_journey_events_user_id_event_at")
|
|
@@index([eventName, eventAt], map: "idx_user_journey_events_event_name_event_at")
|
|
}
|
|
|
|
model DailyActivitySummary {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
activityDate DateTime @db.Date
|
|
lessonsCompleted Int @default(0)
|
|
exercisesSubmitted Int @default(0)
|
|
chatMessagesCount Int @default(0)
|
|
totalStudyMinutes Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, activityDate], map: "uq_daily_activity_summaries_user_id_activity_date")
|
|
}
|
|
|
|
model AiProvider {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
organizationId BigInt? @db.UnsignedBigInt
|
|
key String @unique @db.VarChar(64)
|
|
displayName String @db.VarChar(191)
|
|
endpointBaseUrl String @db.VarChar(512)
|
|
active Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
organization Organization? @relation(fields: [organizationId], references: [id], onDelete: SetNull)
|
|
models AiModel[]
|
|
runs AiAnalysisRun[]
|
|
}
|
|
|
|
model AiModel {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
providerId BigInt @db.UnsignedBigInt
|
|
modelKey String @db.VarChar(128)
|
|
displayName String @db.VarChar(191)
|
|
isDefault Boolean @default(false)
|
|
maxInputTokens Int?
|
|
maxOutputTokens Int?
|
|
active Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
|
|
provider AiProvider @relation(fields: [providerId], references: [id], onDelete: Cascade)
|
|
runs AiAnalysisRun[]
|
|
|
|
@@unique([providerId, modelKey], map: "uq_ai_models_provider_id_model_key")
|
|
}
|
|
|
|
model AiPrompt {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
key String @db.VarChar(100)
|
|
version String @db.VarChar(32)
|
|
promptText String @db.LongText
|
|
active Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
runs AiAnalysisRun[]
|
|
|
|
@@unique([key, version], map: "uq_ai_prompts_key_version")
|
|
}
|
|
|
|
model AiAnalysisRun {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
enrollmentId BigInt? @db.UnsignedBigInt
|
|
providerId BigInt @db.UnsignedBigInt
|
|
modelId BigInt @db.UnsignedBigInt
|
|
promptId BigInt? @db.UnsignedBigInt
|
|
runType AiRunType
|
|
status AiRunStatus @default(queued)
|
|
inputRefType String? @db.VarChar(64)
|
|
inputRefId BigInt? @db.UnsignedBigInt
|
|
startedAt DateTime?
|
|
finishedAt DateTime?
|
|
latencyMs Int?
|
|
totalTokens Int?
|
|
errorCode String? @db.VarChar(64)
|
|
errorMessage String? @db.Text
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
|
|
provider AiProvider @relation(fields: [providerId], references: [id], onDelete: Restrict)
|
|
model AiModel @relation(fields: [modelId], references: [id], onDelete: Restrict)
|
|
prompt AiPrompt? @relation(fields: [promptId], references: [id], onDelete: SetNull)
|
|
outputs AiAnalysisOutput[]
|
|
aiFeedbacks AiFeedback[]
|
|
aiMemories AiMemory[]
|
|
sessionSummaries SessionSummary[]
|
|
|
|
@@index([userId, createdAt], map: "idx_ai_analysis_runs_user_id_created_at")
|
|
@@index([status, createdAt], map: "idx_ai_analysis_runs_status_created_at")
|
|
}
|
|
|
|
model AiAnalysisOutput {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
runId BigInt @db.UnsignedBigInt
|
|
outputType String @db.VarChar(64)
|
|
outputText String? @db.LongText
|
|
outputJson Json?
|
|
createdAt DateTime @default(now())
|
|
|
|
run AiAnalysisRun @relation(fields: [runId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([runId], map: "idx_ai_analysis_outputs_run_id")
|
|
}
|
|
|
|
model AiMemory {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
enrollmentId BigInt? @db.UnsignedBigInt
|
|
lessonId BigInt? @db.UnsignedBigInt
|
|
sourceRunId BigInt? @db.UnsignedBigInt
|
|
memoryType String @db.VarChar(64)
|
|
summaryText String @db.Text
|
|
summaryJson Json?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
|
|
lesson Lesson? @relation(fields: [lessonId], references: [id], onDelete: SetNull)
|
|
sourceRun AiAnalysisRun? @relation(fields: [sourceRunId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId, createdAt], map: "idx_ai_memories_user_id_created_at")
|
|
@@index([enrollmentId], map: "idx_ai_memories_enrollment_id")
|
|
@@map("ai_memories")
|
|
}
|
|
|
|
model SessionSummary {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
enrollmentId BigInt @db.UnsignedBigInt
|
|
lessonId BigInt @db.UnsignedBigInt
|
|
sessionId BigInt @unique @db.UnsignedBigInt
|
|
sourceRunId BigInt? @db.UnsignedBigInt
|
|
summaryText String @db.Text
|
|
summaryJson Json?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
enrollment ProgramEnrollment @relation(fields: [enrollmentId], references: [id], onDelete: Cascade)
|
|
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Restrict)
|
|
session ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
|
sourceRun AiAnalysisRun? @relation(fields: [sourceRunId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId, createdAt], map: "idx_session_summaries_user_id_created_at")
|
|
@@index([enrollmentId, lessonId], map: "idx_session_summaries_enrollment_id_lesson_id")
|
|
@@map("session_summaries")
|
|
}
|
|
|
|
model ChatSession {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
enrollmentId BigInt @db.UnsignedBigInt
|
|
programVersionId BigInt @db.UnsignedBigInt
|
|
moduleId BigInt @db.UnsignedBigInt
|
|
lessonId BigInt @db.UnsignedBigInt
|
|
lessonProgressId BigInt? @db.UnsignedBigInt
|
|
title String? @db.VarChar(191)
|
|
startedAt DateTime @default(now())
|
|
endedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
enrollment ProgramEnrollment @relation(fields: [enrollmentId, userId, programVersionId], references: [id, userId, programVersionId], onDelete: Cascade)
|
|
programVersion ProgramVersion @relation(fields: [programVersionId], references: [id], onDelete: Restrict)
|
|
module Module @relation(fields: [moduleId, programVersionId], references: [id, programVersionId], onDelete: Restrict)
|
|
lesson Lesson @relation(fields: [lessonId, moduleId], references: [id, moduleId], onDelete: Restrict)
|
|
lessonProgress UserLessonProgress? @relation(fields: [lessonProgressId], references: [id], onDelete: SetNull)
|
|
messages ChatMessage[]
|
|
summary SessionSummary?
|
|
|
|
@@index([userId, startedAt], map: "idx_chat_sessions_user_id_started_at")
|
|
@@index([userId, enrollmentId], map: "idx_chat_sessions_user_id_enrollment_id")
|
|
@@index([enrollmentId, lessonId], map: "idx_chat_sessions_enrollment_id_lesson_id")
|
|
}
|
|
|
|
model ChatMessage {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
sessionId BigInt @db.UnsignedBigInt
|
|
userId BigInt? @db.UnsignedBigInt
|
|
role ChatRole
|
|
content String @db.LongText
|
|
tokenCount Int?
|
|
moderationFlag Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
session ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([sessionId, createdAt], map: "idx_chat_messages_session_id_created_at")
|
|
@@index([userId, createdAt], map: "idx_chat_messages_user_id_created_at")
|
|
}
|
|
|
|
model AiFeedback {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
lessonProgressId BigInt? @db.UnsignedBigInt
|
|
submissionId BigInt? @db.UnsignedBigInt
|
|
runId BigInt? @db.UnsignedBigInt
|
|
feedbackType String @db.VarChar(64)
|
|
summary String? @db.Text
|
|
feedbackJson Json?
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
lessonProgress UserLessonProgress? @relation(fields: [lessonProgressId], references: [id], onDelete: SetNull)
|
|
submission UserExerciseSubmission? @relation(fields: [submissionId], references: [id], onDelete: SetNull)
|
|
run AiAnalysisRun? @relation(fields: [runId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId, createdAt], map: "idx_ai_feedback_user_id_created_at")
|
|
}
|
|
|
|
model NotificationQueue {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
enrollmentId BigInt? @db.UnsignedBigInt
|
|
type String @db.VarChar(100)
|
|
title String @db.VarChar(191)
|
|
body String @db.Text
|
|
payload Json?
|
|
scheduledAt DateTime
|
|
status NotificationQueueStatus @default(pending)
|
|
sentAt DateTime?
|
|
attemptCount Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
enrollment ProgramEnrollment? @relation(fields: [enrollmentId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId, status, scheduledAt], map: "idx_notification_queue_user_status_scheduled")
|
|
@@index([status, scheduledAt], map: "idx_notification_queue_status_scheduled")
|
|
@@map("notification_queue")
|
|
}
|
|
|
|
model BehaviorLog {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt? @db.UnsignedBigInt
|
|
eventName String @db.VarChar(100)
|
|
source String @db.VarChar(64)
|
|
payload Json?
|
|
ipAddress String? @db.VarChar(45)
|
|
userAgent String? @db.VarChar(512)
|
|
createdAt DateTime @default(now())
|
|
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([eventName, createdAt], map: "idx_behavior_logs_event_name_created_at")
|
|
@@index([userId, createdAt], map: "idx_behavior_logs_user_id_created_at")
|
|
}
|
|
|
|
model AuditLog {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
actorUserId BigInt? @db.UnsignedBigInt
|
|
action String @db.VarChar(100)
|
|
resourceType String @db.VarChar(100)
|
|
resourceId String? @db.VarChar(191)
|
|
metadata Json?
|
|
createdAt DateTime @default(now())
|
|
|
|
actor User? @relation(fields: [actorUserId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([action, createdAt], map: "idx_audit_logs_action_created_at")
|
|
@@index([actorUserId, createdAt], map: "idx_audit_logs_actor_user_id_created_at")
|
|
}
|
|
|
|
model AdminReportSnapshot {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
reportDate DateTime @db.Date
|
|
metricKey String @db.VarChar(100)
|
|
metricValue Decimal @db.Decimal(16, 4)
|
|
dimensions Json?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([reportDate, metricKey], map: "idx_admin_report_snapshots_report_date_metric_key")
|
|
}
|