refactor: P1-2 actions 层 DB 操作下沉到 data-access (exams/homework/questions/announcements)
This commit is contained in:
@@ -2,55 +2,21 @@
|
||||
|
||||
import { requirePermission, PermissionDeniedError } from "@/shared/lib/auth-guard";
|
||||
import { Permissions } from "@/shared/types/permissions";
|
||||
import { db } from "@/shared/db";
|
||||
import { chapters, knowledgePoints, questions, questionsToKnowledgePoints, textbooks } 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, asc, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { getQuestions, type GetQuestionsParams } from "./data-access";
|
||||
import {
|
||||
createQuestionWithRelations,
|
||||
deleteQuestionByIdRecursive,
|
||||
getKnowledgePointOptions,
|
||||
getQuestions,
|
||||
updateQuestionById,
|
||||
type GetQuestionsParams,
|
||||
} from "./data-access";
|
||||
import type { KnowledgePointOption } from "./types";
|
||||
|
||||
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
|
||||
@@ -81,9 +47,7 @@ export async function createNestedQuestion(
|
||||
|
||||
const input = validatedFields.data;
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await insertQuestionWithRelations(tx, input, ctx.userId, null);
|
||||
});
|
||||
await createQuestionWithRelations(input, ctx.userId);
|
||||
|
||||
revalidatePath("/teacher/questions");
|
||||
|
||||
@@ -114,7 +78,7 @@ 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(),
|
||||
content: z.unknown(),
|
||||
knowledgePointIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
@@ -140,32 +104,9 @@ export async function updateQuestionAction(
|
||||
};
|
||||
}
|
||||
|
||||
const input = parsed.data;
|
||||
const { id, ...updateData } = 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, ctx.userId)));
|
||||
|
||||
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,
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
await updateQuestionById(id, updateData, canEditAll, ctx.userId);
|
||||
|
||||
revalidatePath("/teacher/questions");
|
||||
|
||||
@@ -181,19 +122,6 @@ export async function updateQuestionAction(
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -207,21 +135,7 @@ export async function deleteQuestionAction(
|
||||
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 !== ctx.userId) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
await deleteQuestionRecursive(tx, questionId);
|
||||
});
|
||||
await deleteQuestionByIdRecursive(questionId, canDeleteAll, ctx.userId);
|
||||
|
||||
revalidatePath("/teacher/questions");
|
||||
|
||||
@@ -252,39 +166,7 @@ export async function getQuestionsAction(params: GetQuestionsParams) {
|
||||
export async function getKnowledgePointOptionsAction(): Promise<KnowledgePointOption[]> {
|
||||
try {
|
||||
await requirePermission(Permissions.QUESTION_READ);
|
||||
|
||||
const rows = await db
|
||||
.select({
|
||||
id: knowledgePoints.id,
|
||||
name: knowledgePoints.name,
|
||||
chapterId: chapters.id,
|
||||
chapterTitle: chapters.title,
|
||||
textbookId: textbooks.id,
|
||||
textbookTitle: textbooks.title,
|
||||
subject: textbooks.subject,
|
||||
grade: textbooks.grade,
|
||||
})
|
||||
.from(knowledgePoints)
|
||||
.leftJoin(chapters, eq(chapters.id, knowledgePoints.chapterId))
|
||||
.leftJoin(textbooks, eq(textbooks.id, chapters.textbookId))
|
||||
.orderBy(
|
||||
asc(textbooks.title),
|
||||
asc(chapters.order),
|
||||
asc(chapters.title),
|
||||
asc(knowledgePoints.order),
|
||||
asc(knowledgePoints.name)
|
||||
);
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
chapterId: row.chapterId ?? null,
|
||||
chapterTitle: row.chapterTitle ?? null,
|
||||
textbookId: row.textbookId ?? null,
|
||||
textbookTitle: row.textbookTitle ?? null,
|
||||
subject: row.subject ?? null,
|
||||
grade: row.grade ?? null,
|
||||
}));
|
||||
return await getKnowledgePointOptions();
|
||||
} catch (e) {
|
||||
if (e instanceof PermissionDeniedError) {
|
||||
throw e;
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import 'server-only';
|
||||
|
||||
import { db } from "@/shared/db";
|
||||
import { questions, questionsToKnowledgePoints } from "@/shared/db/schema";
|
||||
import { and, count, desc, eq, inArray, sql, type SQL } from "drizzle-orm";
|
||||
import { chapters, knowledgePoints, questions, questionsToKnowledgePoints, textbooks } from "@/shared/db/schema";
|
||||
import { and, asc, count, desc, eq, inArray, sql, type SQL } from "drizzle-orm";
|
||||
import { cache } from "react";
|
||||
import type { Question, QuestionType } from "./types";
|
||||
import { createId } from "@paralleldrive/cuid2";
|
||||
import type { CreateQuestionInput } from "./schema";
|
||||
import type { KnowledgePointOption, Question, QuestionType } from "./types";
|
||||
|
||||
type Tx = Parameters<Parameters<typeof db.transaction>[0]>[0]
|
||||
|
||||
export type UpdateQuestionInput = {
|
||||
type: QuestionType
|
||||
difficulty: number
|
||||
content: unknown
|
||||
knowledgePointIds?: string[]
|
||||
}
|
||||
|
||||
export type GetQuestionsParams = {
|
||||
q?: string;
|
||||
@@ -136,3 +147,153 @@ export const getQuestionsDashboardStats = cache(async (): Promise<QuestionsDashb
|
||||
const [row] = await db.select({ value: count() }).from(questions)
|
||||
return { questionCount: Number(row?.value ?? 0) }
|
||||
})
|
||||
|
||||
async function insertQuestionWithRelations(
|
||||
tx: Tx,
|
||||
input: CreateQuestionInput,
|
||||
authorId: string,
|
||||
parentId: string | null = null
|
||||
): Promise<string> {
|
||||
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 createQuestionWithRelations(
|
||||
input: CreateQuestionInput,
|
||||
authorId: string
|
||||
): Promise<string> {
|
||||
return await db.transaction(async (tx) => {
|
||||
return await insertQuestionWithRelations(tx, input, authorId, null);
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateQuestionById(
|
||||
id: string,
|
||||
input: UpdateQuestionInput,
|
||||
canEditAll: boolean,
|
||||
authorId: string
|
||||
): Promise<void> {
|
||||
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, id)
|
||||
: and(eq(questions.id, id), eq(questions.authorId, authorId))
|
||||
);
|
||||
|
||||
await tx
|
||||
.delete(questionsToKnowledgePoints)
|
||||
.where(eq(questionsToKnowledgePoints.questionId, id));
|
||||
|
||||
if (input.knowledgePointIds && input.knowledgePointIds.length > 0) {
|
||||
await tx.insert(questionsToKnowledgePoints).values(
|
||||
input.knowledgePointIds.map((kpId) => ({
|
||||
questionId: id,
|
||||
knowledgePointId: kpId,
|
||||
}))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteQuestionRecursive(tx: Tx, questionId: string): Promise<void> {
|
||||
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 deleteQuestionByIdRecursive(
|
||||
questionId: string,
|
||||
canDeleteAll: boolean,
|
||||
authorId: string
|
||||
): Promise<void> {
|
||||
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 !== authorId) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
await deleteQuestionRecursive(tx, questionId);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getKnowledgePointOptions(): Promise<KnowledgePointOption[]> {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: knowledgePoints.id,
|
||||
name: knowledgePoints.name,
|
||||
chapterId: chapters.id,
|
||||
chapterTitle: chapters.title,
|
||||
textbookId: textbooks.id,
|
||||
textbookTitle: textbooks.title,
|
||||
subject: textbooks.subject,
|
||||
grade: textbooks.grade,
|
||||
})
|
||||
.from(knowledgePoints)
|
||||
.leftJoin(chapters, eq(chapters.id, knowledgePoints.chapterId))
|
||||
.leftJoin(textbooks, eq(textbooks.id, chapters.textbookId))
|
||||
.orderBy(
|
||||
asc(textbooks.title),
|
||||
asc(chapters.order),
|
||||
asc(chapters.title),
|
||||
asc(knowledgePoints.order),
|
||||
asc(knowledgePoints.name)
|
||||
);
|
||||
|
||||
return rows.map((row) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
chapterId: row.chapterId ?? null,
|
||||
chapterTitle: row.chapterTitle ?? null,
|
||||
textbookId: row.textbookId ?? null,
|
||||
textbookTitle: row.textbookTitle ?? null,
|
||||
subject: row.subject ?? null,
|
||||
grade: row.grade ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user