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[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 => { 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 { 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 { 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 { 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 { 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 { 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 { 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, })); }