Files
pwa/backend/prisma/schema.prisma
T
ar.badieiandCursor 370ac15471 Ship Novya PWA with backend for deployable production use.
Fix production TypeScript builds and add DEPLOY.md so the app can be hosted from this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 21:01:52 +03:30

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