Files
NextEdu/src/modules/questions/data-access.ts

300 lines
7.5 KiB
TypeScript

import 'server-only';
import { db } from "@/shared/db";
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 { 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;
page?: number;
pageSize?: number;
ids?: string[];
knowledgePointId?: string;
type?: QuestionType;
difficulty?: number;
};
export const getQuestions = cache(async ({
q,
page = 1,
pageSize = 50,
ids,
knowledgePointId,
type,
difficulty,
}: GetQuestionsParams = {}) => {
const offset = (page - 1) * pageSize;
const conditions: SQL[] = [];
if (ids && ids.length > 0) {
conditions.push(inArray(questions.id, ids));
}
if (q && q.trim().length > 0) {
const needle = `%${q.trim().toLowerCase()}%`;
conditions.push(
sql`LOWER(CAST(${questions.content} AS CHAR)) LIKE ${needle}`
);
}
if (type) {
conditions.push(eq(questions.type, type));
}
if (difficulty) {
conditions.push(eq(questions.difficulty, difficulty));
}
if (knowledgePointId) {
const subQuery = db
.select({ questionId: questionsToKnowledgePoints.questionId })
.from(questionsToKnowledgePoints)
.where(eq(questionsToKnowledgePoints.knowledgePointId, knowledgePointId));
conditions.push(inArray(questions.id, subQuery));
}
if (!ids || ids.length === 0) {
conditions.push(sql`${questions.parentId} IS NULL`)
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
const [totalResult] = await db
.select({ count: count() })
.from(questions)
.where(whereClause);
const total = Number(totalResult?.count ?? 0);
const rows = await db.query.questions.findMany({
where: whereClause,
limit: pageSize,
offset: offset,
orderBy: [desc(questions.createdAt)],
with: {
knowledgePoints: {
with: {
knowledgePoint: true,
},
},
author: {
columns: {
id: true,
name: true,
image: true,
},
},
children: true,
},
});
return {
data: rows.map((row) => {
const knowledgePoints =
row.knowledgePoints?.map((rel) => rel.knowledgePoint) ?? [];
const author = row.author
? {
id: row.author.id,
name: row.author.name,
image: row.author.image,
}
: null;
const mapped: Question = {
id: row.id,
content: row.content,
type: row.type,
difficulty: row.difficulty ?? 1,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
author,
knowledgePoints: knowledgePoints.map((kp) => ({ id: kp.id, name: kp.name })),
childrenCount: row.children?.length ?? 0,
};
return mapped;
}),
meta: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
},
};
});
export type QuestionsDashboardStats = {
questionCount: number
}
export const getQuestionsDashboardStats = cache(async (): Promise<QuestionsDashboardStats> => {
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,
}));
}