247 lines
6.7 KiB
TypeScript
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);
|
|
}
|