feat: add TestsService create flow

Made-with: Cursor
This commit is contained in:
Anton
2026-03-04 14:44:54 +03:00
parent 9fbb6431d8
commit dacfad308c

View File

@@ -0,0 +1,149 @@
import { eq } from 'drizzle-orm';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import type * as schema from '../../db/schema/index.js';
import { tests, testQuestions } from '../../db/schema/index.js';
import { AppError, ERROR_CODES } from '../../utils/errors.js';
import type { Stack, Level, TestMode } from '../../db/schema/enums.js';
import type { QuestionService } from '../questions/question.service.js';
type Db = NodePgDatabase<typeof schema>;
export type CreateTestInput = {
stack: Stack;
level: Level;
questionCount: number;
mode?: TestMode;
};
export type TestSnapshot = {
id: string;
testId: string;
questionBankId: string | null;
orderNumber: number;
type: string;
questionText: string;
options: Array<{ key: string; text: string }> | null;
correctAnswer: string | string[];
explanation: string;
userAnswer?: string | string[] | null;
isCorrect?: boolean | null;
answeredAt?: string | null;
};
export type TestWithQuestions = {
id: string;
userId: string;
stack: string;
level: string;
questionCount: number;
mode: string;
status: string;
score: number | null;
startedAt: string;
finishedAt: string | null;
timeLimitSeconds: number | null;
questions: TestSnapshot[];
};
export class TestsService {
constructor(
private readonly db: Db,
private readonly questionService: QuestionService
) {}
/**
* Create a new test: fetch questions, create test record, snapshot questions into test_questions.
*/
async createTest(userId: string, input: CreateTestInput): Promise<TestWithQuestions> {
const { stack, level, questionCount, mode = 'fixed' } = input;
if (questionCount < 1 || questionCount > 50) {
throw new AppError(
ERROR_CODES.BAD_REQUEST,
'questionCount must be between 1 and 50',
400
);
}
const questions = await this.questionService.getQuestionsForTest(
userId,
stack,
level,
questionCount
);
const [test] = await this.db
.insert(tests)
.values({
userId,
stack,
level,
questionCount,
mode,
status: 'in_progress',
})
.returning();
if (!test) {
throw new AppError(
ERROR_CODES.INTERNAL_ERROR,
'Failed to create test',
500
);
}
const tqValues = questions.map((q, i) => ({
testId: test.id,
questionBankId: q.questionBankId,
orderNumber: i + 1,
type: q.type,
questionText: q.questionText,
options: q.options,
correctAnswer: q.correctAnswer,
explanation: q.explanation,
}));
await this.db.insert(testQuestions).values(tqValues);
const questionsRows = await this.db
.select()
.from(testQuestions)
.where(eq(testQuestions.testId, test.id))
.orderBy(testQuestions.orderNumber);
return this.toTestWithQuestions(test, questionsRows);
}
private toTestWithQuestions(
test: (typeof tests.$inferSelect),
questionsRows: (typeof testQuestions.$inferSelect)[]
): TestWithQuestions {
return {
id: test.id,
userId: test.userId,
stack: test.stack,
level: test.level,
questionCount: test.questionCount,
mode: test.mode,
status: test.status,
score: test.score,
startedAt: test.startedAt.toISOString(),
finishedAt: test.finishedAt?.toISOString() ?? null,
timeLimitSeconds: test.timeLimitSeconds,
questions: questionsRows.map((q) => ({
id: q.id,
testId: q.testId,
questionBankId: q.questionBankId,
orderNumber: q.orderNumber,
type: q.type,
questionText: q.questionText,
options: q.options,
correctAnswer: q.correctAnswer,
explanation: q.explanation,
userAnswer: q.userAnswer,
isCorrect: q.isCorrect,
answeredAt: q.answeredAt?.toISOString() ?? null,
})),
};
}
}