feat: exam actions and data safety fixes
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { db } from "@/shared/db"
|
||||
import { exams, examQuestions, examSubmissions, submissionAnswers, users } from "@/shared/db/schema"
|
||||
import { exams, examQuestions, examSubmissions, submissionAnswers } from "@/shared/db/schema"
|
||||
import { eq, desc, like, and, or } from "drizzle-orm"
|
||||
import { cache } from "react"
|
||||
|
||||
import type { ExamStatus } from "./types"
|
||||
import type { Exam, ExamDifficulty, ExamStatus } from "./types"
|
||||
|
||||
export type GetExamsParams = {
|
||||
q?: string
|
||||
@@ -13,6 +13,40 @@ export type GetExamsParams = {
|
||||
pageSize?: number
|
||||
}
|
||||
|
||||
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null
|
||||
|
||||
const parseExamMeta = (description: string | null): Record<string, unknown> => {
|
||||
if (!description) return {}
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(description)
|
||||
return isRecord(parsed) ? parsed : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const getString = (obj: Record<string, unknown>, key: string): string | undefined => {
|
||||
const v = obj[key]
|
||||
return typeof v === "string" ? v : undefined
|
||||
}
|
||||
|
||||
const getNumber = (obj: Record<string, unknown>, key: string): number | undefined => {
|
||||
const v = obj[key]
|
||||
return typeof v === "number" ? v : undefined
|
||||
}
|
||||
|
||||
const getStringArray = (obj: Record<string, unknown>, key: string): string[] | undefined => {
|
||||
const v = obj[key]
|
||||
if (!Array.isArray(v)) return undefined
|
||||
const items = v.filter((x): x is string => typeof x === "string")
|
||||
return items.length === v.length ? items : undefined
|
||||
}
|
||||
|
||||
const toExamDifficulty = (n: number | undefined): ExamDifficulty => {
|
||||
if (n === 1 || n === 2 || n === 3 || n === 4 || n === 5) return n
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
export const getExams = cache(async (params: GetExamsParams) => {
|
||||
const conditions = []
|
||||
@@ -23,7 +57,7 @@ export const getExams = cache(async (params: GetExamsParams) => {
|
||||
}
|
||||
|
||||
if (params.status && params.status !== "all") {
|
||||
conditions.push(eq(exams.status, params.status as any))
|
||||
conditions.push(eq(exams.status, params.status))
|
||||
}
|
||||
|
||||
// Note: Difficulty is stored in JSON description field in current schema,
|
||||
@@ -37,25 +71,23 @@ export const getExams = cache(async (params: GetExamsParams) => {
|
||||
})
|
||||
|
||||
// Transform and Filter (especially for JSON fields)
|
||||
let result = data.map((exam) => {
|
||||
let meta: any = {}
|
||||
try {
|
||||
meta = JSON.parse(exam.description || "{}")
|
||||
} catch { }
|
||||
let result: Exam[] = data.map((exam) => {
|
||||
const meta = parseExamMeta(exam.description || null)
|
||||
|
||||
return {
|
||||
id: exam.id,
|
||||
title: exam.title,
|
||||
status: (exam.status as ExamStatus) || "draft",
|
||||
subject: meta.subject || "General",
|
||||
grade: meta.grade || "General",
|
||||
difficulty: meta.difficulty || 1,
|
||||
totalScore: meta.totalScore || 100,
|
||||
durationMin: meta.durationMin || 60,
|
||||
questionCount: meta.questionCount || 0,
|
||||
subject: getString(meta, "subject") || "General",
|
||||
grade: getString(meta, "grade") || "General",
|
||||
difficulty: toExamDifficulty(getNumber(meta, "difficulty")),
|
||||
totalScore: getNumber(meta, "totalScore") || 100,
|
||||
durationMin: getNumber(meta, "durationMin") || 60,
|
||||
questionCount: getNumber(meta, "questionCount") || 0,
|
||||
scheduledAt: exam.startTime?.toISOString(),
|
||||
createdAt: exam.createdAt.toISOString(),
|
||||
tags: meta.tags || [],
|
||||
updatedAt: exam.updatedAt?.toISOString(),
|
||||
tags: getStringArray(meta, "tags") || [],
|
||||
}
|
||||
})
|
||||
|
||||
@@ -82,30 +114,27 @@ export const getExamById = cache(async (id: string) => {
|
||||
|
||||
if (!exam) return null
|
||||
|
||||
let meta: any = {}
|
||||
try {
|
||||
meta = JSON.parse(exam.description || "{}")
|
||||
} catch { }
|
||||
const meta = parseExamMeta(exam.description || null)
|
||||
|
||||
return {
|
||||
id: exam.id,
|
||||
title: exam.title,
|
||||
status: (exam.status as ExamStatus) || "draft",
|
||||
subject: meta.subject || "General",
|
||||
grade: meta.grade || "General",
|
||||
difficulty: meta.difficulty || 1,
|
||||
totalScore: meta.totalScore || 100,
|
||||
durationMin: meta.durationMin || 60,
|
||||
subject: getString(meta, "subject") || "General",
|
||||
grade: getString(meta, "grade") || "General",
|
||||
difficulty: toExamDifficulty(getNumber(meta, "difficulty")),
|
||||
totalScore: getNumber(meta, "totalScore") || 100,
|
||||
durationMin: getNumber(meta, "durationMin") || 60,
|
||||
scheduledAt: exam.startTime?.toISOString(),
|
||||
createdAt: exam.createdAt.toISOString(),
|
||||
tags: meta.tags || [],
|
||||
structure: exam.structure as any, // Return structure
|
||||
questions: exam.questions.map(eq => ({
|
||||
id: eq.questionId,
|
||||
score: eq.score,
|
||||
order: eq.order,
|
||||
// ... include question details if needed
|
||||
}))
|
||||
updatedAt: exam.updatedAt?.toISOString(),
|
||||
tags: getStringArray(meta, "tags") || [],
|
||||
structure: exam.structure as unknown,
|
||||
questions: exam.questions.map((eqRel) => ({
|
||||
id: eqRel.questionId,
|
||||
score: eqRel.score ?? 0,
|
||||
order: eqRel.order ?? 0,
|
||||
})),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -154,13 +183,20 @@ export const getSubmissionDetails = cache(async (submissionId: string) => {
|
||||
orderBy: [desc(examQuestions.order)],
|
||||
})
|
||||
|
||||
type QuestionContent = { text?: string } & Record<string, unknown>
|
||||
|
||||
const toQuestionContent = (v: unknown): QuestionContent | null => {
|
||||
if (!isRecord(v)) return null
|
||||
return v as QuestionContent
|
||||
}
|
||||
|
||||
// Map answers with question details
|
||||
const answersWithDetails = answers.map(ans => {
|
||||
const eqRel = examQ.find(q => q.questionId === ans.questionId)
|
||||
return {
|
||||
id: ans.id,
|
||||
questionId: ans.questionId,
|
||||
questionContent: ans.question.content,
|
||||
questionContent: toQuestionContent(ans.question.content),
|
||||
questionType: ans.question.type,
|
||||
maxScore: eqRel?.score || 0,
|
||||
studentAnswer: ans.answerContent,
|
||||
|
||||
Reference in New Issue
Block a user