feat: add AdminQuestionService

Made-with: Cursor
This commit is contained in:
Anton
2026-03-04 14:56:20 +03:00
parent 91b33f6f41
commit 9bada23e2e

View File

@@ -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<typeof schema>;
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<ListPendingResult> {
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<void> {
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<void> {
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<PendingQuestion> {
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,
};
}
}