Files
CICD/src/modules/questions/actions.ts

247 lines
6.7 KiB
TypeScript

"use server";
import { db } from "@/shared/db";
import { questions, questionsToKnowledgePoints } from "@/shared/db/schema";
import { CreateQuestionSchema } from "./schema";
import type { CreateQuestionInput } from "./schema";
import { ActionState } from "@/shared/types/action-state";
import { revalidatePath } from "next/cache";
import { createId } from "@paralleldrive/cuid2";
import { and, eq } from "drizzle-orm";
import { z } from "zod";
import { getQuestions, type GetQuestionsParams } from "./data-access";
async function getCurrentUser() {
return {
id: "user_teacher_math",
role: "teacher",
};
}
async function ensureTeacher() {
const user = await getCurrentUser();
if (!user || (user.role !== "teacher" && user.role !== "admin")) {
throw new Error("Unauthorized: Only teachers can perform this action.");
}
return user;
}
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
async function insertQuestionWithRelations(
tx: Tx,
input: z.infer<typeof CreateQuestionSchema>,
authorId: string,
parentId: string | null = null
) {
const newQuestionId = createId();
await tx.insert(questions).values({
id: newQuestionId,
content: input.content,
type: input.type,
difficulty: input.difficulty,
authorId: authorId,
parentId: parentId,
});
if (input.knowledgePointIds && input.knowledgePointIds.length > 0) {
await tx.insert(questionsToKnowledgePoints).values(
input.knowledgePointIds.map((kpId) => ({
questionId: newQuestionId,
knowledgePointId: kpId,
}))
);
}
if (input.subQuestions && input.subQuestions.length > 0) {
for (const subQ of input.subQuestions) {
await insertQuestionWithRelations(tx, subQ, authorId, newQuestionId);
}
}
return newQuestionId;
}
export async function createNestedQuestion(
prevState: ActionState<string> | undefined,
formData: FormData | CreateQuestionInput
): Promise<ActionState<string>> {
try {
const user = await ensureTeacher();
let rawInput: unknown = formData;
if (formData instanceof FormData) {
const jsonString = formData.get("json");
if (typeof jsonString === "string") {
rawInput = JSON.parse(jsonString) as unknown;
} else {
return { success: false, message: "Invalid submission format. Expected JSON." };
}
}
const validatedFields = CreateQuestionSchema.safeParse(rawInput);
if (!validatedFields.success) {
return {
success: false,
message: "Validation failed",
errors: validatedFields.error.flatten().fieldErrors,
};
}
const input = validatedFields.data;
await db.transaction(async (tx) => {
await insertQuestionWithRelations(tx, input, user.id, null);
});
revalidatePath("/teacher/questions");
return {
success: true,
message: "Question created successfully",
};
} catch (error) {
if (error instanceof Error) {
return {
success: false,
message: error.message || "Database error occurred",
};
}
return {
success: false,
message: "An unexpected error occurred",
};
}
}
const UpdateQuestionSchema = z.object({
id: z.string().min(1),
type: z.enum(["single_choice", "multiple_choice", "text", "judgment", "composite"]),
difficulty: z.number().min(1).max(5),
content: z.any(),
knowledgePointIds: z.array(z.string()).optional(),
});
export async function updateQuestionAction(
prevState: ActionState<string> | undefined,
formData: FormData
): Promise<ActionState<string>> {
try {
const user = await ensureTeacher();
const canEditAll = user.role === "admin";
const jsonString = formData.get("json");
if (typeof jsonString !== "string") {
return { success: false, message: "Invalid submission format. Expected JSON." };
}
const parsed = UpdateQuestionSchema.safeParse(JSON.parse(jsonString));
if (!parsed.success) {
return {
success: false,
message: "Validation failed",
errors: parsed.error.flatten().fieldErrors,
};
}
const input = parsed.data;
await db.transaction(async (tx) => {
await tx
.update(questions)
.set({
type: input.type,
difficulty: input.difficulty,
content: input.content,
updatedAt: new Date(),
})
.where(canEditAll ? eq(questions.id, input.id) : and(eq(questions.id, input.id), eq(questions.authorId, user.id)));
await tx
.delete(questionsToKnowledgePoints)
.where(eq(questionsToKnowledgePoints.questionId, input.id));
if (input.knowledgePointIds && input.knowledgePointIds.length > 0) {
await tx.insert(questionsToKnowledgePoints).values(
input.knowledgePointIds.map((kpId) => ({
questionId: input.id,
knowledgePointId: kpId,
}))
);
}
});
revalidatePath("/teacher/questions");
return { success: true, message: "Question updated successfully" };
} catch (error) {
if (error instanceof Error) {
return { success: false, message: error.message };
}
return { success: false, message: "An unexpected error occurred" };
}
}
async function deleteQuestionRecursive(tx: Tx, questionId: string) {
const children = await tx
.select({ id: questions.id })
.from(questions)
.where(eq(questions.parentId, questionId));
for (const child of children) {
await deleteQuestionRecursive(tx, child.id);
}
await tx.delete(questions).where(eq(questions.id, questionId));
}
export async function deleteQuestionAction(
prevState: ActionState<string> | undefined,
formData: FormData
): Promise<ActionState<string>> {
try {
const user = await ensureTeacher();
const canDeleteAll = user.role === "admin";
const questionId = formData.get("questionId");
if (typeof questionId !== "string") {
return { success: false, message: "Invalid question ID" };
}
await db.transaction(async (tx) => {
const q = await tx.query.questions.findFirst({
where: eq(questions.id, questionId),
});
if (!q) {
throw new Error("Question not found");
}
if (!canDeleteAll && q.authorId !== user.id) {
throw new Error("Unauthorized");
}
await deleteQuestionRecursive(tx, questionId);
});
revalidatePath("/teacher/questions");
return { success: true, message: "Question deleted successfully" };
} catch (error) {
if (error instanceof Error) {
return { success: false, message: error.message };
}
return { success: false, message: "Failed to delete question" };
}
}
export async function getQuestionsAction(params: GetQuestionsParams) {
await ensureTeacher();
return await getQuestions(params);
}