Fix production TypeScript builds and add DEPLOY.md so the app can be hosted from this repo. Co-authored-by: Cursor <cursoragent@cursor.com>
271 lines
8.6 KiB
Plaintext
271 lines
8.6 KiB
Plaintext
// Novya backend schema.
|
|
// Identity/auth from Phase 3 is preserved. Domain tables match BACKEND_DATABASE_ARCHITECTURE_AUDIT.md.
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "mysql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
enum AuthProvider {
|
|
password
|
|
guest
|
|
}
|
|
|
|
enum AttemptPhase {
|
|
pre
|
|
post
|
|
}
|
|
|
|
enum AttemptStatus {
|
|
/// Reserved for future draft/in-progress attempts; MVP always writes `scored`.
|
|
submitted
|
|
scored
|
|
}
|
|
|
|
enum SessionProgressStatus {
|
|
not_started
|
|
in_progress
|
|
completed
|
|
}
|
|
|
|
enum ChatRole {
|
|
/// Used in coach prompts at runtime; not persisted to chat_messages.
|
|
system
|
|
user
|
|
assistant
|
|
}
|
|
|
|
enum AiRunStatus {
|
|
queued
|
|
running
|
|
succeeded
|
|
failed
|
|
}
|
|
|
|
model User {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
externalId String @unique @default(uuid()) @db.Char(36)
|
|
isGuest Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
profile UserProfile?
|
|
identities AuthIdentity[]
|
|
sessions AuthSession[]
|
|
journey LearningJourney?
|
|
assessmentAttempts AssessmentAttempt[]
|
|
sessionProgress SessionProgress[]
|
|
chatSessions ChatSession[]
|
|
aiRuns AiAnalysisRun[]
|
|
|
|
@@index([createdAt], map: "idx_users_created_at")
|
|
@@index([isGuest], map: "idx_users_is_guest")
|
|
@@map("users")
|
|
}
|
|
|
|
model UserProfile {
|
|
userId BigInt @id @db.UnsignedBigInt
|
|
name String @db.VarChar(100)
|
|
age Int
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([name], map: "idx_user_profiles_name")
|
|
@@map("user_profiles")
|
|
}
|
|
|
|
model AuthIdentity {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
provider AuthProvider
|
|
providerSubject String @db.VarChar(191)
|
|
email String? @db.VarChar(191)
|
|
passwordHash String? @db.VarChar(255)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([provider, providerSubject], map: "uq_auth_identities_provider_subject")
|
|
@@index([userId], map: "idx_auth_identities_user_id")
|
|
@@index([email], map: "idx_auth_identities_email")
|
|
@@map("auth_identities")
|
|
}
|
|
|
|
model AuthSession {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
tokenHash String @unique @db.Char(64)
|
|
expiresAt DateTime
|
|
revokedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId, expiresAt], map: "idx_auth_sessions_user_id_expires_at")
|
|
@@index([expiresAt], map: "idx_auth_sessions_expires_at")
|
|
@@map("auth_sessions")
|
|
}
|
|
|
|
/// Timestamps are authoritative. currentStep is a denormalized cache.
|
|
model LearningJourney {
|
|
userId BigInt @id @db.UnsignedBigInt
|
|
currentStep String @default("registered") @db.VarChar(32)
|
|
preTestCompletedAt DateTime?
|
|
trainingStartedAt DateTime?
|
|
trainingCompletedAt DateTime?
|
|
postTestCompletedAt DateTime?
|
|
lastActivityAt DateTime @default(now())
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("learning_journeys")
|
|
}
|
|
|
|
/// Catalog of the 11 educational sessions. Content blocks are JSON (same shape as PWA).
|
|
model CourseSession {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
sessionKey String @unique @db.VarChar(64)
|
|
order Int @unique
|
|
title String @db.VarChar(191)
|
|
description String @db.Text
|
|
estimatedDurationMinutes Int
|
|
blocks Json
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
sessionProgress SessionProgress[]
|
|
chatSessions ChatSession[]
|
|
|
|
@@index([order], map: "idx_course_sessions_order")
|
|
@@map("course_sessions")
|
|
}
|
|
|
|
model SessionProgress {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
sessionOrder Int
|
|
status SessionProgressStatus @default(not_started)
|
|
startedAt DateTime?
|
|
completedAt DateTime?
|
|
completionMetadata Json?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
courseSession CourseSession @relation(fields: [sessionOrder], references: [order], onDelete: Restrict, onUpdate: Cascade)
|
|
|
|
@@unique([userId, sessionOrder], map: "uq_session_progress_user_order")
|
|
@@index([userId, status], map: "idx_session_progress_user_status")
|
|
@@map("session_progress")
|
|
}
|
|
|
|
model AssessmentAttempt {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
phase AttemptPhase
|
|
status AttemptStatus @default(scored)
|
|
submittedAt DateTime
|
|
scoredAt DateTime
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
answers AssessmentAnswer[]
|
|
scores AssessmentScore[]
|
|
|
|
@@unique([userId, phase], map: "uq_assessment_attempts_user_phase")
|
|
@@index([userId, phase, submittedAt], map: "idx_assessment_attempts_user_phase_submitted")
|
|
@@map("assessment_attempts")
|
|
}
|
|
|
|
model AssessmentAnswer {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
attemptId BigInt @db.UnsignedBigInt
|
|
questionId String @db.VarChar(32)
|
|
rawValue Int
|
|
points Int
|
|
dimension String @db.VarChar(32)
|
|
isReverseScored Boolean
|
|
createdAt DateTime @default(now())
|
|
|
|
attempt AssessmentAttempt @relation(fields: [attemptId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([attemptId, questionId], map: "uq_assessment_answers_attempt_question")
|
|
@@map("assessment_answers")
|
|
}
|
|
|
|
model AssessmentScore {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
attemptId BigInt @db.UnsignedBigInt
|
|
metricKey String @db.VarChar(32)
|
|
rawScore Int
|
|
createdAt DateTime @default(now())
|
|
|
|
attempt AssessmentAttempt @relation(fields: [attemptId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([attemptId, metricKey], map: "uq_assessment_scores_attempt_metric")
|
|
@@map("assessment_scores")
|
|
}
|
|
|
|
model ChatSession {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
sessionOrder Int
|
|
startedAt DateTime @default(now())
|
|
endedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
courseSession CourseSession @relation(fields: [sessionOrder], references: [order], onDelete: Restrict, onUpdate: Cascade)
|
|
messages ChatMessage[]
|
|
aiRuns AiAnalysisRun[]
|
|
|
|
@@index([userId, sessionOrder, startedAt], map: "idx_chat_sessions_user_order_started")
|
|
@@map("chat_sessions")
|
|
}
|
|
|
|
model ChatMessage {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
chatSessionId BigInt @db.UnsignedBigInt
|
|
role ChatRole
|
|
content String @db.Text
|
|
metadata Json?
|
|
createdAt DateTime @default(now())
|
|
|
|
chatSession ChatSession @relation(fields: [chatSessionId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([chatSessionId, createdAt], map: "idx_chat_messages_session_created")
|
|
@@map("chat_messages")
|
|
}
|
|
|
|
model AiAnalysisRun {
|
|
id BigInt @id @default(autoincrement()) @db.UnsignedBigInt
|
|
userId BigInt @db.UnsignedBigInt
|
|
chatSessionId BigInt?
|
|
provider String @db.VarChar(64)
|
|
model String @db.VarChar(128)
|
|
status AiRunStatus @default(queued)
|
|
latencyMs Int?
|
|
totalTokens Int?
|
|
errorMessage String? @db.Text
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
chatSession ChatSession? @relation(fields: [chatSessionId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId, createdAt], map: "idx_ai_analysis_runs_user_created")
|
|
@@map("ai_analysis_runs")
|
|
}
|