refactor: P1-2 actions 层 DB 操作下沉到 data-access (exams/homework/questions/announcements)
This commit is contained in:
@@ -2,6 +2,7 @@ import { db } from "@/shared/db"
|
||||
import { exams, examQuestions, questions, subjects, grades, classes } from "@/shared/db/schema"
|
||||
import { count, eq, desc, like, and, or, inArray } from "drizzle-orm"
|
||||
import { cache } from "react"
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
|
||||
import type { Exam, ExamDifficulty, ExamStatus } from "./types"
|
||||
import type { AiGeneratedQuestion, AiGeneratedStructureNode } from "./ai-pipeline"
|
||||
@@ -371,3 +372,153 @@ export const getExamsDashboardStats = cache(async (scope?: DataScope): Promise<E
|
||||
|
||||
return { examCount: Number(row?.value ?? 0) }
|
||||
})
|
||||
|
||||
/**
|
||||
* Get exam creator ID for ownership check.
|
||||
* Returns null if exam not found.
|
||||
*/
|
||||
export const getExamCreatorId = async (examId: string): Promise<string | null> => {
|
||||
const exam = await db.query.exams.findFirst({
|
||||
where: eq(exams.id, examId),
|
||||
columns: { creatorId: true },
|
||||
})
|
||||
return exam?.creatorId ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an exam, optionally replacing its questions.
|
||||
* Preserves original behavior: questions replacement is not transactional with exam field update.
|
||||
*/
|
||||
export const updateExamWithQuestions = async (
|
||||
examId: string,
|
||||
data: {
|
||||
questions?: Array<{ id: string; score: number }>
|
||||
structure?: unknown
|
||||
status?: ExamStatus
|
||||
}
|
||||
): Promise<void> => {
|
||||
if (data.questions) {
|
||||
await db.delete(examQuestions).where(eq(examQuestions.examId, examId))
|
||||
if (data.questions.length > 0) {
|
||||
await db.insert(examQuestions).values(
|
||||
data.questions.map((q, idx) => ({
|
||||
examId,
|
||||
questionId: q.id,
|
||||
score: q.score ?? 0,
|
||||
order: idx,
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const updateData: Partial<typeof exams.$inferInsert> = {}
|
||||
if (data.status) updateData.status = data.status
|
||||
if (data.structure !== undefined) updateData.structure = data.structure
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await db.update(exams).set(updateData).where(eq(exams.id, examId))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an exam by ID.
|
||||
*/
|
||||
export const deleteExamById = async (examId: string): Promise<void> => {
|
||||
await db.delete(exams).where(eq(exams.id, examId))
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate an exam (including its questions) in a transaction.
|
||||
* Returns the new exam ID, or null if the source exam is not found.
|
||||
*/
|
||||
export const duplicateExam = async (
|
||||
sourceExamId: string,
|
||||
newCreatorId: string
|
||||
): Promise<string | null> => {
|
||||
const source = await db.query.exams.findFirst({
|
||||
where: eq(exams.id, sourceExamId),
|
||||
with: {
|
||||
questions: {
|
||||
orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!source) return null
|
||||
|
||||
const newExamId = createId()
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(exams).values({
|
||||
id: newExamId,
|
||||
title: `${source.title} (Copy)`,
|
||||
description: omitScheduledAtFromDescription(source.description),
|
||||
creatorId: newCreatorId,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
status: "draft",
|
||||
structure: source.structure,
|
||||
})
|
||||
|
||||
if (source.questions.length > 0) {
|
||||
await tx.insert(examQuestions).values(
|
||||
source.questions.map((q) => ({
|
||||
examId: newExamId,
|
||||
questionId: q.questionId,
|
||||
score: q.score ?? 0,
|
||||
order: q.order ?? 0,
|
||||
}))
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return newExamId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get exam preview data (structure + questions).
|
||||
* Returns null if exam not found.
|
||||
*/
|
||||
export const getExamPreview = async (
|
||||
examId: string
|
||||
): Promise<{ structure: unknown; questions: Array<{ id: string }> } | null> => {
|
||||
const exam = await db.query.exams.findFirst({
|
||||
where: eq(exams.id, examId),
|
||||
with: {
|
||||
questions: {
|
||||
orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
|
||||
with: {
|
||||
question: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!exam) return null
|
||||
|
||||
const questions = exam.questions.map((eqRel) => eqRel.question)
|
||||
return {
|
||||
structure: exam.structure,
|
||||
questions,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all subjects for exam forms.
|
||||
*/
|
||||
export const getExamSubjects = async (): Promise<Array<{ id: string; name: string }>> => {
|
||||
const allSubjects = await db.query.subjects.findMany({
|
||||
orderBy: (subjects, { asc }) => [asc(subjects.order), asc(subjects.name)],
|
||||
})
|
||||
return allSubjects.map((s) => ({ id: s.id, name: s.name }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all grades for exam forms.
|
||||
*/
|
||||
export const getExamGrades = async (): Promise<Array<{ id: string; name: string }>> => {
|
||||
const allGrades = await db.query.grades.findMany({
|
||||
orderBy: (grades, { asc }) => [asc(grades.order), asc(grades.name)],
|
||||
})
|
||||
return allGrades.map((g) => ({ id: g.id, name: g.name }))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user