From 9bada23e2eeb998cf769852de9a214d51595377c Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 4 Mar 2026 14:56:20 +0300 Subject: [PATCH] feat: add AdminQuestionService Made-with: Cursor --- src/services/admin/admin-question.service.ts | 172 +++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/services/admin/admin-question.service.ts diff --git a/src/services/admin/admin-question.service.ts b/src/services/admin/admin-question.service.ts new file mode 100644 index 0000000..70c4080 --- /dev/null +++ b/src/services/admin/admin-question.service.ts @@ -0,0 +1,172 @@ +import { eq, asc, count } from 'drizzle-orm'; +import type { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import type * as schema from '../../db/schema/index.js'; +import { questionBank } from '../../db/schema/index.js'; +import { notFound } from '../../utils/errors.js'; +import type { Stack, Level, QuestionType } from '../../db/schema/enums.js'; + +type Db = NodePgDatabase; + +export type PendingQuestion = { + id: string; + stack: Stack; + level: Level; + type: QuestionType; + questionText: string; + options: Array<{ key: string; text: string }> | null; + correctAnswer: string | string[]; + explanation: string; + source: string; + createdAt: Date; +}; + +export type EditQuestionInput = { + stack?: Stack; + level?: Level; + type?: QuestionType; + questionText?: string; + options?: Array<{ key: string; text: string }> | null; + correctAnswer?: string | string[]; + explanation?: string; +}; + +export type ListPendingResult = { + questions: PendingQuestion[]; + total: number; +}; + +export class AdminQuestionService { + constructor(private readonly db: Db) {} + + async listPending(limit = 50, offset = 0): Promise { + const questions = await this.db + .select({ + id: questionBank.id, + stack: questionBank.stack, + level: questionBank.level, + type: questionBank.type, + questionText: questionBank.questionText, + options: questionBank.options, + correctAnswer: questionBank.correctAnswer, + explanation: questionBank.explanation, + source: questionBank.source, + createdAt: questionBank.createdAt, + }) + .from(questionBank) + .where(eq(questionBank.status, 'pending')) + .orderBy(asc(questionBank.createdAt)) + .limit(limit) + .offset(offset); + + const [{ count: totalCount }] = await this.db + .select({ count: count() }) + .from(questionBank) + .where(eq(questionBank.status, 'pending')); + + return { + questions: questions.map((q) => ({ + id: q.id, + stack: q.stack, + level: q.level, + type: q.type, + questionText: q.questionText, + options: q.options, + correctAnswer: q.correctAnswer, + explanation: q.explanation, + source: q.source, + createdAt: q.createdAt, + })), + total: totalCount ?? 0, + }; + } + + async approve(questionId: string): Promise { + const [updated] = await this.db + .update(questionBank) + .set({ + status: 'approved', + approvedAt: new Date(), + }) + .where(eq(questionBank.id, questionId)) + .returning({ id: questionBank.id }); + + if (!updated) { + throw notFound('Question not found'); + } + } + + async reject(questionId: string): Promise { + const [updated] = await this.db + .update(questionBank) + .set({ status: 'rejected' }) + .where(eq(questionBank.id, questionId)) + .returning({ id: questionBank.id }); + + if (!updated) { + throw notFound('Question not found'); + } + } + + async edit(questionId: string, input: EditQuestionInput): Promise { + const updates: Partial<{ + stack: Stack; + level: Level; + type: QuestionType; + questionText: string; + options: Array<{ key: string; text: string }> | null; + correctAnswer: string | string[]; + explanation: string; + }> = {}; + + if (input.stack !== undefined) updates.stack = input.stack; + if (input.level !== undefined) updates.level = input.level; + if (input.type !== undefined) updates.type = input.type; + if (input.questionText !== undefined) updates.questionText = input.questionText; + if (input.options !== undefined) updates.options = input.options; + if (input.correctAnswer !== undefined) updates.correctAnswer = input.correctAnswer; + if (input.explanation !== undefined) updates.explanation = input.explanation; + + if (Object.keys(updates).length === 0) { + const [existing] = await this.db + .select() + .from(questionBank) + .where(eq(questionBank.id, questionId)); + if (!existing) throw notFound('Question not found'); + return { + id: existing.id, + stack: existing.stack, + level: existing.level, + type: existing.type, + questionText: existing.questionText, + options: existing.options, + correctAnswer: existing.correctAnswer, + explanation: existing.explanation, + source: existing.source, + createdAt: existing.createdAt, + }; + } + + const [updated] = await this.db + .update(questionBank) + .set(updates) + .where(eq(questionBank.id, questionId)) + .returning(); + + if (!updated) { + throw notFound('Question not found'); + } + + return { + id: updated.id, + stack: updated.stack, + level: updated.level, + type: updated.type, + questionText: updated.questionText, + options: updated.options, + correctAnswer: updated.correctAnswer, + explanation: updated.explanation, + source: updated.source, + createdAt: updated.createdAt, + }; + } +}