Files
CICD/src/modules/exams/data-access.ts
SpecialX eb08c0ab68
All checks were successful
CI / build-deploy (push) Successful in 4m39s
sync-docs-and-fixes
2026-03-03 17:32:26 +08:00

161 lines
4.9 KiB
TypeScript

import { db } from "@/shared/db"
import { exams } from "@/shared/db/schema"
import { eq, desc, like, and, or } from "drizzle-orm"
import { cache } from "react"
import type { Exam, ExamDifficulty, ExamStatus } from "./types"
export type GetExamsParams = {
q?: string
status?: string
difficulty?: string
page?: number
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 = []
if (params.q) {
const search = `%${params.q}%`
conditions.push(or(like(exams.title, search), like(exams.description, search)))
}
if (params.status && params.status !== "all") {
conditions.push(eq(exams.status, params.status))
}
// Note: Difficulty is stored in JSON description field in current schema,
// so we might need to filter in memory or adjust schema.
// For now, let's fetch and filter in memory if difficulty is needed,
// or just ignore strict DB filtering for JSON fields to keep it simple.
const data = await db.query.exams.findMany({
where: conditions.length ? and(...conditions) : undefined,
orderBy: [desc(exams.createdAt)],
with: {
subject: true,
gradeEntity: true,
}
})
// Transform and Filter (especially for JSON fields)
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: exam.subject?.name ?? getString(meta, "subject") ?? "General",
grade: exam.gradeEntity?.name ?? 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(),
updatedAt: exam.updatedAt?.toISOString(),
tags: getStringArray(meta, "tags") || [],
}
})
if (params.difficulty && params.difficulty !== "all") {
const d = parseInt(params.difficulty)
result = result.filter((e) => e.difficulty === d)
}
return result
})
export const getExamById = cache(async (id: string) => {
const exam = await db.query.exams.findFirst({
where: eq(exams.id, id),
with: {
subject: true,
gradeEntity: true,
questions: {
orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
with: {
question: true
}
}
}
})
if (!exam) return null
const meta = parseExamMeta(exam.description || null)
return {
id: exam.id,
title: exam.title,
status: (exam.status as ExamStatus) || "draft",
subject: exam.subject?.name ?? getString(meta, "subject") ?? "General",
grade: exam.gradeEntity?.name ?? 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(),
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,
})),
}
})
export const omitScheduledAtFromDescription = (description: string | null): string => {
if (!description) return "{}"
try {
const meta = JSON.parse(description)
if (typeof meta === "object" && meta !== null) {
const rest = { ...(meta as Record<string, unknown>) }
delete rest.scheduledAt
return JSON.stringify(rest)
}
return description
} catch {
return description || "{}"
}
}