import type { FastifyInstance } from 'fastify'; import { LlmService } from '../services/llm/llm.service.js'; import { QuestionService } from '../services/questions/question.service.js'; import { TestsService } from '../services/tests/tests.service.js'; import { notFound } from '../utils/errors.js'; import type { CreateTestInput } from '../services/tests/tests.service.js'; import type { TestSnapshot } from '../services/tests/tests.service.js'; const STACKS = ['html', 'css', 'js', 'ts', 'react', 'vue', 'nodejs', 'git', 'web_basics'] as const; const LEVELS = ['basic', 'beginner', 'intermediate', 'advanced', 'expert'] as const; const createTestSchema = { body: { type: 'object', required: ['stack', 'level', 'questionCount'], properties: { stack: { type: 'string', enum: STACKS }, level: { type: 'string', enum: LEVELS }, questionCount: { type: 'integer', minimum: 1, maximum: 50 }, mode: { type: 'string', enum: ['fixed', 'infinite', 'marathon'] }, }, additionalProperties: false, }, }; const testIdParamsSchema = { params: { type: 'object', required: ['testId'], properties: { testId: { type: 'string', format: 'uuid' }, }, }, }; const answerSchema = { params: { type: 'object', required: ['testId'], properties: { testId: { type: 'string', format: 'uuid' }, }, }, body: { type: 'object', required: ['questionId', 'answer'], properties: { questionId: { type: 'string', format: 'uuid' }, answer: { oneOf: [ { type: 'string' }, { type: 'array', items: { type: 'string' } }, ], }, }, additionalProperties: false, }, }; const historyQuerySchema = { querystring: { type: 'object', properties: { limit: { type: 'integer', minimum: 1, maximum: 100 }, offset: { type: 'integer', minimum: 0 }, }, additionalProperties: false, }, }; function stripAnswersForClient( q: TestSnapshot ): Omit { const { correctAnswer: _, explanation: __, ...rest } = q; return rest; } export async function testsRoutes(app: FastifyInstance) { const llmService = new LlmService(); const questionService = new QuestionService(app.db, llmService); const testsService = new TestsService(app.db, questionService); const { rateLimitOptions } = app; app.get( '/', { schema: historyQuerySchema, config: { rateLimit: rateLimitOptions.apiAuthed }, preHandler: [app.authenticate], }, async (req, reply) => { const userId = req.user!.id; const { limit, offset } = (req.query as { limit?: number; offset?: number }) ?? {}; const { tests: testList, total } = await testsService.getHistory( userId, { limit, offset } ); return reply.send({ tests: testList, total }); } ); app.post( '/', { schema: createTestSchema, config: { rateLimit: rateLimitOptions.apiAuthed }, preHandler: [app.authenticate, app.withSubscription], }, async (req, reply) => { const userId = req.user!.id; const body = req.body as CreateTestInput; const test = await testsService.createTest(userId, body); const hideAnswers = test.status === 'in_progress'; const response = { ...test, questions: hideAnswers ? test.questions.map(stripAnswersForClient) : test.questions, }; return reply.status(201).send(response); } ); app.post( '/:testId/answer', { schema: answerSchema, config: { rateLimit: rateLimitOptions.apiAuthed }, preHandler: [app.authenticate], }, async (req, reply) => { const userId = req.user!.id; const { testId } = req.params as { testId: string }; const { questionId, answer } = req.body as { questionId: string; answer: string | string[] }; const snapshot = await testsService.answerQuestion( userId, testId, questionId, answer ); return reply.send(snapshot); } ); app.post( '/:testId/finish', { schema: testIdParamsSchema, config: { rateLimit: rateLimitOptions.apiAuthed }, preHandler: [app.authenticate], }, async (req, reply) => { const userId = req.user!.id; const { testId } = req.params as { testId: string }; const test = await testsService.finishTest(userId, testId); return reply.send(test); } ); app.get( '/:testId', { schema: testIdParamsSchema, config: { rateLimit: rateLimitOptions.apiAuthed }, preHandler: [app.authenticate], }, async (req, reply) => { const userId = req.user!.id; const { testId } = req.params as { testId: string }; const test = await testsService.getById(userId, testId); if (!test) { throw notFound('Test not found'); } const hideAnswers = test.status === 'in_progress'; const response = { ...test, questions: hideAnswers ? test.questions.map(stripAnswersForClient) : test.questions, }; return reply.send(response); } ); }