300 lines
7.5 KiB
TypeScript
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,
|
|
}));
|
|
}
|