feat add questions module and admin routes

This commit is contained in:
2026-06-15 07:34:41 +03:30
parent d1fecf62ad
commit 0eea63fec1
28 changed files with 636 additions and 23 deletions
@@ -0,0 +1,194 @@
import {
ConflictException,
HttpStatus,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { QueryFailedError, Repository } from 'typeorm';
import { Question } from '../entities/question.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { CreateQuestionDTO } from '../dtos/create-question.dto';
import { TestsService } from '@/modules/tests/providers/tests.service';
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
import { AppResponse, PaginatedResponse } from '@/common/responses';
import { I18nService } from 'nestjs-i18n';
import { UNIQUE_TEST_ORDER_IDX } from '../constants';
import { FindQuestionsDTO } from '../dtos/find-questions.dto';
import { filterQuestions } from '../helpers/filter-questions.helper';
import Utils from '@/common/utils';
import { Test } from '@/modules/tests/entities/test.entity';
import { UpdateQuestionDTO } from '../dtos/update-question.dto';
import { validateMetadata } from '../helpers/validate-metadata.helper';
import { AppException } from '@/common/exceptions/app.exception';
@Injectable()
export class QuestionsService {
constructor(
/**
* Inject Questions Repository
*/
@InjectRepository(Question)
private readonly questionsRepository: Repository<Question>,
/**
* Inject Tests Service
*/
private readonly testsService: TestsService,
/**
* Inject I18n Service
*/
private readonly i18nService: I18nService,
) {}
public async createOne(createQuestionDto: CreateQuestionDTO) {
const test = await this.testsService.adminGetOne(createQuestionDto.test);
if (!test) {
throw new NotFoundException('tests.errors.testNotFound');
}
const newQuestion = this.questionsRepository.create({
...createQuestionDto,
test,
});
try {
await this.questionsRepository.save(newQuestion);
return new AppResponse(
newQuestion,
this.i18nService.translate('questions.messages.createSuccess'),
);
} catch (err) {
if (
err instanceof QueryFailedError &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(err as any).constraint === UNIQUE_TEST_ORDER_IDX
) {
throw new ConflictException('questions.errors.uniqueOrderConstraint');
}
throw new AppTimeoutException(err);
}
}
public async findAllForAdmin(query: FindQuestionsDTO) {
const filters = filterQuestions(query);
const { limit, page, skip, take } =
Utils.PaginationUtils.getPaginationOptions(query);
let selectedTest: Test | null = null;
if (query.test) {
try {
selectedTest = await this.testsService.adminGetOne(query.test);
} catch (err) {
throw new AppTimeoutException(err);
}
}
try {
const [questions, total] = await this.questionsRepository.findAndCount({
where: {
...filters,
...(selectedTest && {
test: selectedTest,
}),
},
loadRelationIds: true,
skip,
take,
order: query.sortBy
? {
[query.sortBy]: query.order ?? 'ASC',
}
: { createdAt: 'DESC' },
});
return new PaginatedResponse(questions, {
page,
limit,
total,
});
} catch (err) {
throw new AppTimeoutException(err);
}
}
public async findOneForAdmin(id: number) {
try {
return await this.questionsRepository.findOne({
where: { id },
relations: { test: true },
});
} catch (err) {
throw new AppTimeoutException(err);
}
}
public async updateOne(id: number, updateQuestionDto: UpdateQuestionDTO) {
const question = await this.questionsRepository.findOneBy({ id });
if (!question) {
throw new NotFoundException('questions.errors.questionNotFound');
}
const type =
updateQuestionDto.type !== undefined
? updateQuestionDto.type
: question.type;
const metadata =
updateQuestionDto.metadata !== undefined
? updateQuestionDto.metadata
: question.metadata;
const errors = await validateMetadata(type, metadata);
if (errors && errors.length > 0) {
throw new AppException(
'validation.wrongFieldFormat',
HttpStatus.BAD_REQUEST,
{ field: '$t(questions.fields.metadata)' },
);
}
Object.assign(question, updateQuestionDto);
console.log(question);
try {
return await this.questionsRepository.save(question);
} catch (err) {
if (
err instanceof QueryFailedError &&
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(err as any).constraint === UNIQUE_TEST_ORDER_IDX
) {
throw new ConflictException('questions.errors.uniqueOrderConstraint');
}
throw new AppTimeoutException(err);
}
}
public async deleteOne(id: number) {
let question: Question | null = null;
try {
question = await this.questionsRepository.findOneBy({ id });
} catch (err) {
throw new AppTimeoutException(err);
}
if (!question)
throw new NotFoundException('questions.errors.questionNotFound');
try {
await this.questionsRepository.delete(question.id);
} catch (err) {
console.log(err);
throw new AppTimeoutException(err);
}
}
}