feat: add TestsService create flow
Made-with: Cursor
This commit is contained in:
149
src/services/tests/tests.service.ts
Normal file
149
src/services/tests/tests.service.ts
Normal 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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user