From 0274722f6c7578b156b778a24c8cc9d8a73044cc Mon Sep 17 00:00:00 2001 From: radmehr Date: Sat, 1 Aug 2026 17:03:17 +0330 Subject: [PATCH] feat: add question transition first validations --- src/i18n/en/questions.json | 10 +- src/i18n/fa/questions.json | 10 +- .../admin/choices/admin-choices.controller.ts | 2 +- .../questions/admin-questions.controller.ts | 2 +- .../entities/question-transition.entity.ts | 4 +- .../providers/choices-command.provider.ts | 149 ----------- .../providers/choices-query.provider.ts | 85 ------- .../questions/providers/choices.service.ts | 41 --- .../question-range-rule-command.provider.ts | 196 --------------- .../question-range-rule-query.provider.ts | 84 ------- .../providers/question-range-rule.service.ts | 51 ---- .../question-transition-command.provider.ts | 0 .../question-transition-query.provider.ts | 0 .../question-transition-validator.provider.ts | 233 ------------------ .../providers/question-transition.service.ts | 0 src/modules/questions/questions.module.ts | 12 +- 16 files changed, 26 insertions(+), 853 deletions(-) delete mode 100644 src/modules/questions/providers/choices-command.provider.ts delete mode 100644 src/modules/questions/providers/choices-query.provider.ts delete mode 100644 src/modules/questions/providers/choices.service.ts delete mode 100644 src/modules/questions/providers/question-range-rule-command.provider.ts delete mode 100644 src/modules/questions/providers/question-range-rule-query.provider.ts delete mode 100644 src/modules/questions/providers/question-range-rule.service.ts delete mode 100644 src/modules/questions/providers/question-transition-command.provider.ts delete mode 100644 src/modules/questions/providers/question-transition-query.provider.ts delete mode 100644 src/modules/questions/providers/question-transition-validator.provider.ts delete mode 100644 src/modules/questions/providers/question-transition.service.ts diff --git a/src/i18n/en/questions.json b/src/i18n/en/questions.json index 7e6ec1f..44922f5 100644 --- a/src/i18n/en/questions.json +++ b/src/i18n/en/questions.json @@ -12,12 +12,18 @@ "choice": "Choice", "score": "Score", "weight": "Weight", - "rangeRule": "Range Rule" + "rangeRule": "Range Rule", + "questionTransition": "Question Transition" }, "errors": { "uniqueOrderConstraint": "Order value should be unique.", "rangeRuleMinMaxConstraint": "min value should be less than max.", - "rangeRuleIntersectionConstraint": "{min} to {max} range has some intersection with other ranges." + "rangeRuleIntersectionConstraint": "{min} to {max} range has some intersection with other ranges.", + "questionTransition": { + "sameQuestion": "fromQuestion and toQuestion are the same", + "fromQuestionNotBelongToTest": "fromQuestion doesnt belong to the test.", + "toQuestionNotBelongToTest": "toQuestion doesnt belong to the test." + } }, "messages": { "createSuccess": "Question Created Successfully.", diff --git a/src/i18n/fa/questions.json b/src/i18n/fa/questions.json index 21b210f..4e7435e 100644 --- a/src/i18n/fa/questions.json +++ b/src/i18n/fa/questions.json @@ -12,12 +12,18 @@ "choice": "گزینه", "score": "نمره", "weight": "وزن", - "rangeRule": "قانون بازه ای" + "rangeRule": "قانون بازه ای", + "questionTransition": "مسیر سوال" }, "errors": { "uniqueOrderConstraint": "مقدار اولویت تکراری است.", "rangeRuleMinMaxConstraint": "مقدار min باید از max کوچکتر باشد.", - "rangeRuleIntersectionConstraint": "بازه {min} تا {max} با دیگر بازه های تعریف شده تداخل دارد." + "rangeRuleIntersectionConstraint": "بازه {min} تا {max} با دیگر بازه های تعریف شده تداخل دارد.", + "questionTransition": { + "sameQuestion": "سوال مبدا و مقصد یکی است.", + "fromQuestionNotBelongToTest": "سوال مبدا متعلق به تست نیست.", + "toQuestionNotBelongToTest": "سوال مقصد متعلق به تست نیست." + } }, "messages": { "createSuccess": "سوال با موفقیت ایجاد شد.", diff --git a/src/modules/admin/choices/admin-choices.controller.ts b/src/modules/admin/choices/admin-choices.controller.ts index 498497f..18cf2a7 100644 --- a/src/modules/admin/choices/admin-choices.controller.ts +++ b/src/modules/admin/choices/admin-choices.controller.ts @@ -3,7 +3,7 @@ import { Roles } from '@/modules/auth/decorators/roles.decorator'; import { CreateChoiceDTO } from '@/modules/questions/dtos/create-choice.dto'; import { FindChoicesDTO } from '@/modules/questions/dtos/find-choices.dto'; import { UpdateChoiceDTO } from '@/modules/questions/dtos/update-choice.dto'; -import { ChoicesService } from '@/modules/questions/providers/choices.service'; +import { ChoicesService } from '@/modules/questions/providers/choices/choices.service'; import { Body, Controller, diff --git a/src/modules/admin/questions/admin-questions.controller.ts b/src/modules/admin/questions/admin-questions.controller.ts index 524d707..576e0ff 100644 --- a/src/modules/admin/questions/admin-questions.controller.ts +++ b/src/modules/admin/questions/admin-questions.controller.ts @@ -6,7 +6,7 @@ import { FindQuestionRangeRuleDTO } from '@/modules/questions/dtos/find-question import { FindQuestionsDTO } from '@/modules/questions/dtos/find-questions.dto'; import { UpdateQuestionRangeRuleDTO } from '@/modules/questions/dtos/update-question-range-rule.dto'; import { UpdateQuestionDTO } from '@/modules/questions/dtos/update-question.dto'; -import { QuestionRangeRuleService } from '@/modules/questions/providers/question-range-rule.service'; +import { QuestionRangeRuleService } from '@/modules/questions/providers/range-rule/question-range-rule.service'; import { QuestionsService } from '@/modules/questions/providers/questions.service'; import { Body, diff --git a/src/modules/questions/entities/question-transition.entity.ts b/src/modules/questions/entities/question-transition.entity.ts index f1eed03..8996e69 100644 --- a/src/modules/questions/entities/question-transition.entity.ts +++ b/src/modules/questions/entities/question-transition.entity.ts @@ -13,10 +13,10 @@ export class QuestionTransition { test!: Test; @ManyToOne(() => Question, { nullable: true, onDelete: 'CASCADE' }) - fromQuestion?: Question; + fromQuestion!: Question | null; @ManyToOne(() => Question, { nullable: true, onDelete: 'CASCADE' }) - toQuestion?: Question; + toQuestion!: Question | null; @Column({ type: 'enum', diff --git a/src/modules/questions/providers/choices-command.provider.ts b/src/modules/questions/providers/choices-command.provider.ts deleted file mode 100644 index d2a2e76..0000000 --- a/src/modules/questions/providers/choices-command.provider.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { ConflictException, HttpStatus, Injectable } from '@nestjs/common'; -import { CreateChoiceDTO } from '../dtos/create-choice.dto'; -import { QueryFailedError, Repository } from 'typeorm'; -import { Choice } from '../entities/choice.entity'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Question } from '../entities/question.entity'; -import { UNIQUE_QUESTION_ORDER_IDX } from '../constants'; -import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception'; -import { AppResponse } from '@/common/responses'; -import { I18nService } from 'nestjs-i18n'; -import { AppException } from '@/common/exceptions/app.exception'; -import { UpdateChoiceDTO } from '../dtos/update-choice.dto'; - -@Injectable() -export class ChoicesCommandProvider { - constructor( - /** - * Inject Choices Repository - */ - @InjectRepository(Choice) - private readonly choicesRepository: Repository, - - /** - * Inject Questions Repository - */ - @InjectRepository(Question) - private readonly questionsRepository: Repository, - - /** - * Inject I18n Service - */ - private readonly i18nService: I18nService, - ) {} - - public async createOne(createChoiceDto: CreateChoiceDTO) { - const question = await this.questionsRepository.findOneBy({ - id: createChoiceDto.question, - }); - - if (!question) { - throw new AppException( - 'common.errors.entityNotFound', - HttpStatus.NOT_FOUND, - { entity: '$t(questions.fields.question)' }, - ); - } - - const newChoice = this.choicesRepository.create({ - ...createChoiceDto, - question, - }); - - try { - await this.choicesRepository.save(newChoice); - return new AppResponse( - newChoice, - this.i18nService.translate('common.messages.createSuccess', { - args: { entity: '$t(questions.fields.choice)' }, - }), - ); - } catch (err) { - if ( - err instanceof QueryFailedError && - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (err as any).constraint === UNIQUE_QUESTION_ORDER_IDX - ) { - throw new ConflictException('questions.errors.uniqueOrderConstraint'); - } - - throw new AppTimeoutException(err); - } - } - - public async updateOne(id: number, updateChoiceDto: UpdateChoiceDTO) { - const choice = await this.choicesRepository.findOneBy({ id }); - - if (!choice) { - throw new AppException( - 'common.errors.entityNotFound', - HttpStatus.NOT_FOUND, - { - entity: '$t(questions.fields.choice)', - }, - ); - } - - if (updateChoiceDto.question) { - const question = await this.questionsRepository.findOneBy({ - id: updateChoiceDto.question, - }); - - if (!question) { - throw new AppException( - 'common.errors.entityNotFound', - HttpStatus.NOT_FOUND, - { - entity: '$t(questions.fields.question)', - }, - ); - } - } - - Object.assign(choice, updateChoiceDto); - - try { - const updatedChoice = await this.choicesRepository.save(choice); - return new AppResponse( - updatedChoice, - this.i18nService.translate('common.messages.updateSuccess'), - ); - } catch (err) { - if ( - err instanceof QueryFailedError && - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - (err as any).constraint === UNIQUE_QUESTION_ORDER_IDX - ) { - throw new ConflictException('questions.errors.uniqueOrderConstraint'); - } - - throw new AppTimeoutException(err); - } - } - - public async deleteOne(id: number) { - let choice: Choice | null = null; - - try { - choice = await this.choicesRepository.findOneBy({ id }); - } catch (err) { - throw new AppTimeoutException(err); - } - - if (!choice) { - throw new AppException( - 'common.errors.entityNotFound', - HttpStatus.NOT_FOUND, - { - entity: '$t(questions.fields.choice)', - }, - ); - } - - try { - await this.choicesRepository.delete(choice.id); - } catch (err) { - throw new AppTimeoutException(err); - } - } -} diff --git a/src/modules/questions/providers/choices-query.provider.ts b/src/modules/questions/providers/choices-query.provider.ts deleted file mode 100644 index f2fe4f1..0000000 --- a/src/modules/questions/providers/choices-query.provider.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import Utils from '@/common/utils'; -import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception'; -import { PaginatedResponse } from '@/common/responses'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Question } from '../entities/question.entity'; -import { Repository } from 'typeorm'; -import { Choice } from '../entities/choice.entity'; -import { FindChoicesDTO } from '../dtos/find-choices.dto'; -import { filterChoices } from '../helpers/filter-choices.helper'; - -@Injectable() -export class ChoicesQueryProvider { - constructor( - /** - * Inject Choices Repository - */ - @InjectRepository(Choice) - private readonly choicesRepository: Repository, - - /** - * Inject Questions Repository - */ - @InjectRepository(Question) - private readonly questionsRepository: Repository, - ) {} - - public async findAllForAdmin(query: FindChoicesDTO) { - const filters = filterChoices(query); - const { limit, page, skip, take } = - Utils.PaginationUtils.getPaginationOptions(query); - - let selectedQuestion: Question | null = null; - - if (query.question) { - try { - selectedQuestion = await this.questionsRepository.findOneBy({ - id: query.question, - }); - } catch (err) { - throw new AppTimeoutException(err); - } - } - - try { - const [choices, total] = await this.choicesRepository.findAndCount({ - where: { - ...filters, - ...(selectedQuestion && { - question: { - id: selectedQuestion.id, - }, - }), - }, - loadRelationIds: true, - skip, - take, - order: query.sortBy - ? { - [query.sortBy]: query.order ?? 'ASC', - } - : { id: 'DESC' }, - }); - - return new PaginatedResponse(choices, { - page, - limit, - total, - }); - } catch (err) { - throw new AppTimeoutException(err); - } - } - - public async findOneForAdmin(id: number) { - try { - return await this.choicesRepository.findOne({ - where: { id }, - relations: { question: true }, - }); - } catch (err) { - throw new AppTimeoutException(err); - } - } -} diff --git a/src/modules/questions/providers/choices.service.ts b/src/modules/questions/providers/choices.service.ts deleted file mode 100644 index de89300..0000000 --- a/src/modules/questions/providers/choices.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { ChoicesCommandProvider } from './choices-command.provider'; -import { CreateChoiceDTO } from '../dtos/create-choice.dto'; -import { UpdateChoiceDTO } from '../dtos/update-choice.dto'; -import { FindChoicesDTO } from '../dtos/find-choices.dto'; -import { ChoicesQueryProvider } from './choices-query.provider'; - -@Injectable() -export class ChoicesService { - constructor( - /** - * Inject Choices Command Provider - */ - private readonly choicesCommnadProvider: ChoicesCommandProvider, - - /** - * Inject Choices Query Provider - */ - private readonly choicesQueryProvider: ChoicesQueryProvider, - ) {} - - public async createOne(createChoiceDto: CreateChoiceDTO) { - return await this.choicesCommnadProvider.createOne(createChoiceDto); - } - - public async updateOne(id: number, updateChoiceDto: UpdateChoiceDTO) { - return await this.choicesCommnadProvider.updateOne(id, updateChoiceDto); - } - - public async deleteOne(id: number) { - return await this.choicesCommnadProvider.deleteOne(id); - } - - public async findAllForAdmin(query: FindChoicesDTO) { - return await this.choicesQueryProvider.findAllForAdmin(query); - } - - public async findOneForAdmin(id: number) { - return await this.choicesQueryProvider.findOneForAdmin(id); - } -} diff --git a/src/modules/questions/providers/question-range-rule-command.provider.ts b/src/modules/questions/providers/question-range-rule-command.provider.ts deleted file mode 100644 index 78ff7b4..0000000 --- a/src/modules/questions/providers/question-range-rule-command.provider.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { BadRequestException, HttpStatus, Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { QuestionRangeRule } from '../entities/question-range-rule.entity'; -import { Repository } from 'typeorm'; -import { Question } from '../entities/question.entity'; -import { I18nService } from 'nestjs-i18n'; -import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception'; -import { AppException } from '@/common/exceptions/app.exception'; -import { CreateQuestionRangeRuleDTO } from '../dtos/create-question-range-rule.dto'; -import { AppResponse } from '@/common/responses'; -import { UpdateQuestionRangeRuleDTO } from '../dtos/update-question-range-rule.dto'; - -@Injectable() -export class QuestionRangeRuleCommandProvider { - constructor( - /** - * Inject QuestionRangeRules Repository - */ - @InjectRepository(QuestionRangeRule) - private readonly questionRangeRulesRepository: Repository, - - /** - * Inject Questions Repository - */ - @InjectRepository(Question) - private readonly questionsRepository: Repository, - - /** - * Inject I18n Service - */ - private readonly i18nService: I18nService, - ) {} - - private async checkQuestionExistance(questionId: number) { - let question: Question | null = null; - - try { - question = await this.questionsRepository.findOneBy({ - id: questionId, - }); - } catch (err) { - throw new AppTimeoutException(err); - } - - if (!question) { - throw new AppException( - 'common.errors.entityNotFound', - HttpStatus.NOT_FOUND, - { entity: '$t(questions.fields.question)' }, - ); - } - - return question; - } - - private async checkRangeOverlap( - question: number, - min: number, - max: number, - excludeSelf: boolean = false, - ): Promise { - const queryBuilder = this.questionRangeRulesRepository - .createQueryBuilder('rule') - .where('rule.questionId = :question', { question }) - .andWhere( - '(rule.min BETWEEN :min AND :max) OR (rule.max BETWEEN :min AND :max) OR (:min BETWEEN rule.min AND rule.max) OR (:max BETWEEN rule.min AND rule.max)', - { min, max }, - ); - - let overlappingRules: Array; - - try { - overlappingRules = await queryBuilder.getMany(); - } catch (err) { - throw new AppTimeoutException(err); - } - - if (overlappingRules.length > (excludeSelf ? 1 : 0)) { - throw new AppException( - 'questions.errors.rangeRuleIntersectionConstraint', - HttpStatus.CONFLICT, - { min, max }, - ); - } - } - - private async checkRangeRuleExistance(rangeRuleId: number) { - let questionRangeRule: QuestionRangeRule | null = null; - - try { - questionRangeRule = await this.questionRangeRulesRepository.findOne({ - where: { - id: rangeRuleId, - }, - relations: { - question: true, - }, - }); - } catch (err) { - throw new AppTimeoutException(err); - } - - if (!questionRangeRule) { - throw new AppException( - 'common.errors.entityNotFound', - HttpStatus.NOT_FOUND, - { entity: '$t(questions.fields.rangeRule)' }, - ); - } - - return questionRangeRule; - } - - public async createOne( - createQuestionRangeRuleDto: CreateQuestionRangeRuleDTO, - ) { - const question = await this.checkQuestionExistance( - createQuestionRangeRuleDto.question, - ); - - await this.checkRangeOverlap( - question.id, - createQuestionRangeRuleDto.min, - createQuestionRangeRuleDto.max, - ); - - const newQuestionRangeRule = this.questionRangeRulesRepository.create({ - ...createQuestionRangeRuleDto, - question, - }); - - try { - await this.questionRangeRulesRepository.save(newQuestionRangeRule); - return new AppResponse( - newQuestionRangeRule, - this.i18nService.translate('common.messages.createSuccess', { - args: { entity: '$t(questions.fields.rangeRule)' }, - }), - ); - } catch (err) { - throw new AppTimeoutException(err); - } - } - - public async updateOne( - id: number, - updateQuestionRangeRuleDto: UpdateQuestionRangeRuleDTO, - ) { - const questionRangeRule = await this.checkRangeRuleExistance(id); - - if (updateQuestionRangeRuleDto.question) { - await this.checkQuestionExistance(updateQuestionRangeRuleDto.question); - } - - const questionId = - updateQuestionRangeRuleDto.question !== undefined - ? updateQuestionRangeRuleDto.question - : questionRangeRule.question.id; - - const min = - updateQuestionRangeRuleDto.min !== undefined - ? updateQuestionRangeRuleDto.min - : questionRangeRule.min; - - const max = - updateQuestionRangeRuleDto.max !== undefined - ? updateQuestionRangeRuleDto.max - : questionRangeRule.max; - - if (min >= max) { - throw new BadRequestException( - 'questions.errors.rangeRuleMinMaxConstraint', - ); - } - - await this.checkRangeOverlap(questionId, min, max, true); - - Object.assign(questionRangeRule, updateQuestionRangeRuleDto); - - try { - return await this.questionRangeRulesRepository.save(questionRangeRule); - } catch (err) { - throw new AppTimeoutException(err); - } - } - - public async deleteOne(id: number) { - const questionRangeRule = await this.checkRangeRuleExistance(id); - - try { - await this.questionRangeRulesRepository.delete(questionRangeRule.id); - } catch (err) { - throw new AppTimeoutException(err); - } - } -} diff --git a/src/modules/questions/providers/question-range-rule-query.provider.ts b/src/modules/questions/providers/question-range-rule-query.provider.ts deleted file mode 100644 index 5c98bf3..0000000 --- a/src/modules/questions/providers/question-range-rule-query.provider.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { FindQuestionRangeRuleDTO } from '../dtos/find-question-range-rule.dto'; -import { filterQuestionRangeRules } from '../helpers/filter-question-range-rules.helper'; -import Utils from '@/common/utils'; -import { Question } from '../entities/question.entity'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception'; -import { QuestionRangeRule } from '../entities/question-range-rule.entity'; -import { PaginatedResponse } from '@/common/responses'; - -@Injectable() -export class QuestionRangeRuleQueryProvider { - constructor( - /** - * Inject Questions Repository - */ - @InjectRepository(Question) - private readonly questionsRepository: Repository, - - /** - * Inject QuestionRangeRules Repository - */ - @InjectRepository(QuestionRangeRule) - private readonly questionRangeRulesRepository: Repository, - ) {} - - public async findAllForAdmin(query: FindQuestionRangeRuleDTO) { - const filters = filterQuestionRangeRules(query); - const { limit, page, skip, take } = - Utils.PaginationUtils.getPaginationOptions(query); - - let selectedQuestion: Question | null = null; - - if (query.question) { - try { - selectedQuestion = await this.questionsRepository.findOneBy({ - id: query.question, - }); - } catch (err) { - throw new AppTimeoutException(err); - } - } - - try { - const [questionRangeRules, total] = - await this.questionRangeRulesRepository.findAndCount({ - where: { - ...filters, - ...(selectedQuestion && { - question: selectedQuestion, - }), - }, - loadRelationIds: true, - skip, - take, - order: query.sortBy - ? { - [query.sortBy]: query.order ?? 'ASC', - } - : { id: 'DESC' }, - }); - - return new PaginatedResponse(questionRangeRules, { - page, - limit, - total, - }); - } catch (err) { - throw new AppTimeoutException(err); - } - } - - public async findOneForAdmin(id: number) { - try { - return await this.questionRangeRulesRepository.findOne({ - where: { id }, - relations: { question: true }, - }); - } catch (err) { - throw new AppTimeoutException(err); - } - } -} diff --git a/src/modules/questions/providers/question-range-rule.service.ts b/src/modules/questions/providers/question-range-rule.service.ts deleted file mode 100644 index 4f8b9ee..0000000 --- a/src/modules/questions/providers/question-range-rule.service.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { CreateQuestionRangeRuleDTO } from '../dtos/create-question-range-rule.dto'; -import { UpdateQuestionRangeRuleDTO } from '../dtos/update-question-range-rule.dto'; -import { QuestionRangeRuleCommandProvider } from './question-range-rule-command.provider'; -import { FindQuestionRangeRuleDTO } from '../dtos/find-question-range-rule.dto'; -import { QuestionRangeRuleQueryProvider } from './question-range-rule-query.provider'; - -@Injectable() -export class QuestionRangeRuleService { - constructor( - /** - * Inject QuestionRangeRule Command Provider - */ - private readonly questionRangeRuleCommandProvider: QuestionRangeRuleCommandProvider, - - /** - * Inject QuestionRangeRule Query Provider - */ - private readonly questionRangeRuleQueryProvider: QuestionRangeRuleQueryProvider, - ) {} - - public async createOne( - createQuestionRangeRuleDto: CreateQuestionRangeRuleDTO, - ) { - return await this.questionRangeRuleCommandProvider.createOne( - createQuestionRangeRuleDto, - ); - } - - public async updateOne( - id: number, - updateQuestionRangeRuleDto: UpdateQuestionRangeRuleDTO, - ) { - return await this.questionRangeRuleCommandProvider.updateOne( - id, - updateQuestionRangeRuleDto, - ); - } - - public async deleteOne(id: number) { - return await this.questionRangeRuleCommandProvider.deleteOne(id); - } - - public async findAllForAdmin(query: FindQuestionRangeRuleDTO) { - return await this.questionRangeRuleQueryProvider.findAllForAdmin(query); - } - - public async findOneForAdmin(id: number) { - return await this.questionRangeRuleQueryProvider.findOneForAdmin(id); - } -} diff --git a/src/modules/questions/providers/question-transition-command.provider.ts b/src/modules/questions/providers/question-transition-command.provider.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/questions/providers/question-transition-query.provider.ts b/src/modules/questions/providers/question-transition-query.provider.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/questions/providers/question-transition-validator.provider.ts b/src/modules/questions/providers/question-transition-validator.provider.ts deleted file mode 100644 index 13461b3..0000000 --- a/src/modules/questions/providers/question-transition-validator.provider.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { HttpStatus, Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { AppException } from '@/common/exceptions/app.exception'; -import { QuestionTransition } from '../entities/question-transition.entity'; -import { QuestionTransitionType } from '../enums/question-transition-type.enum'; -import { CreateQuestionTransitionDTO } from '../dtos/create-question-transition.dto'; -import { UpdateQuestionTransitionDTO } from '../dtos/update-question-transition.dto'; -import { SingleChoiceCondition } from '../interfaces/single-choice-condition.interface'; -import { NumericCondition } from '../interfaces/numeric-condition.interface'; - -@Injectable() -export class QuestionTransitionValidatorProvider { - constructor( - /** - * Inject Question Transitions Repository - */ - @InjectRepository(QuestionTransition) - private readonly transitionsRepository: Repository, - ) {} - - public async validate( - dto: CreateQuestionTransitionDTO | UpdateQuestionTransitionDTO, - transitionId?: number, - ) { - this.validateCondition(dto); - - await this.validateSingleStart(dto, transitionId); - await this.validateSingleEnd(dto, transitionId); - - await this.validateDefaultTransition(dto, transitionId); - - await this.validateDuplicateCondition(dto, transitionId); - - this.validateQuestions(dto); - } - - private validateCondition( - dto: CreateQuestionTransitionDTO | UpdateQuestionTransitionDTO, - ) { - switch (dto.type) { - case QuestionTransitionType.DEFAULT: { - if (dto.conditionValue) { - throw new AppException( - 'questionTransitions.errors.defaultMustNotHaveCondition', - HttpStatus.BAD_REQUEST, - ); - } - - break; - } - - case QuestionTransitionType.CHOICE: { - if ( - !dto.conditionValue || - typeof (dto.conditionValue as SingleChoiceCondition).choiceId !== - 'number' - ) { - throw new AppException( - 'questionTransitions.errors.invalidChoiceCondition', - HttpStatus.BAD_REQUEST, - ); - } - - break; - } - - case QuestionTransitionType.NUMBER_RANGE: { - const condition = dto.conditionValue as NumericCondition; - - if ( - !condition || - typeof condition.min !== 'number' || - typeof condition.max !== 'number' - ) { - throw new AppException( - 'questionTransitions.errors.invalidNumberRange', - HttpStatus.BAD_REQUEST, - ); - } - - if (condition.min > condition.max) { - throw new AppException( - 'questionTransitions.errors.invalidNumberRange', - HttpStatus.BAD_REQUEST, - ); - } - - break; - } - } - } - - private async validateSingleStart( - dto: CreateQuestionTransitionDTO | UpdateQuestionTransitionDTO, - transitionId?: number, - ) { - if (dto.type !== QuestionTransitionType.START) return; - - const qb = this.transitionsRepository - .createQueryBuilder('t') - .where('t.testId = :testId', { - testId: dto.test, - }) - .andWhere('t.type = :type', { - type: QuestionTransitionType.START, - }); - - if (transitionId) { - qb.andWhere('t.id != :transitionId', { - transitionId, - }); - } - - if (await qb.getExists()) { - throw new AppException( - 'questionTransitions.errors.startAlreadyExists', - HttpStatus.CONFLICT, - ); - } - } - - private async validateSingleEnd( - dto: CreateQuestionTransitionDTO | UpdateQuestionTransitionDTO, - transitionId?: number, - ) { - if (dto.type !== QuestionTransitionType.END) return; - - const qb = this.transitionsRepository - .createQueryBuilder('t') - .where('t.fromQuestionId = :fromQuestionId', { - fromQuestionId: dto.fromQuestion, - }) - .andWhere('t.type = :type', { - type: QuestionTransitionType.END, - }); - - if (transitionId) { - qb.andWhere('t.id != :transitionId', { - transitionId, - }); - } - - if (await qb.getExists()) { - throw new AppException( - 'questionTransitions.errors.endAlreadyExists', - HttpStatus.CONFLICT, - ); - } - } - - private async validateDefaultTransition( - dto: CreateQuestionTransitionDTO | UpdateQuestionTransitionDTO, - transitionId?: number, - ) { - if (dto.type !== QuestionTransitionType.DEFAULT) return; - - const qb = this.transitionsRepository - .createQueryBuilder('t') - .where('t.fromQuestionId = :id', { - id: dto.fromQuestion, - }) - .andWhere('t.type = :type', { - type: QuestionTransitionType.DEFAULT, - }); - - if (transitionId) { - qb.andWhere('t.id != :transitionId', { - transitionId, - }); - } - - const exists = await qb.getExists(); - - if (exists) { - throw new AppException( - 'questionTransitions.errors.defaultAlreadyExists', - HttpStatus.CONFLICT, - ); - } - } - - private async validateDuplicateCondition( - dto: CreateQuestionTransitionDTO | UpdateQuestionTransitionDTO, - transitionId?: number, - ) { - if (dto.type === QuestionTransitionType.DEFAULT || !dto.conditionValue) { - return; - } - - const qb = this.transitionsRepository - .createQueryBuilder('t') - .where('t.fromQuestionId = :id', { - id: dto.fromQuestion, - }) - .andWhere('t.type = :type', { - type: dto.type, - }) - .andWhere('t.conditionValue = :condition', { - condition: JSON.stringify(dto.conditionValue), - }); - - if (transitionId) { - qb.andWhere('t.id != :transitionId', { - transitionId, - }); - } - - const exists = await qb.getExists(); - - if (exists) { - throw new AppException( - 'questionTransitions.errors.duplicateTransition', - HttpStatus.CONFLICT, - ); - } - } - - private validateQuestions( - dto: CreateQuestionTransitionDTO | UpdateQuestionTransitionDTO, - ) { - if ( - dto.fromQuestion && - dto.toQuestion && - dto.fromQuestion === dto.toQuestion - ) { - throw new AppException( - 'questionTransitions.errors.sameQuestion', - HttpStatus.BAD_REQUEST, - ); - } - } -} diff --git a/src/modules/questions/providers/question-transition.service.ts b/src/modules/questions/providers/question-transition.service.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/modules/questions/questions.module.ts b/src/modules/questions/questions.module.ts index b1ce017..a0a0c4c 100644 --- a/src/modules/questions/questions.module.ts +++ b/src/modules/questions/questions.module.ts @@ -7,13 +7,13 @@ import { TestsModule } from '../tests/tests.module'; import { QuestionsService } from './providers/questions.service'; import { QuestionsCommandProvider } from './providers/questions-command.provider'; import { QuestionsQueryProvider } from './providers/questions-query.provider'; -import { ChoicesCommandProvider } from './providers/choices-command.provider'; -import { ChoicesService } from './providers/choices.service'; -import { ChoicesQueryProvider } from './providers/choices-query.provider'; +import { ChoicesCommandProvider } from './providers/choices/choices-command.provider'; +import { ChoicesService } from './providers/choices/choices.service'; +import { ChoicesQueryProvider } from './providers/choices/choices-query.provider'; import { QuestionRangeRule } from './entities/question-range-rule.entity'; -import { QuestionRangeRuleService } from './providers/question-range-rule.service'; -import { QuestionRangeRuleCommandProvider } from './providers/question-range-rule-command.provider'; -import { QuestionRangeRuleQueryProvider } from './providers/question-range-rule-query.provider'; +import { QuestionRangeRuleService } from './providers/range-rule/question-range-rule.service'; +import { QuestionRangeRuleCommandProvider } from './providers/range-rule/question-range-rule-command.provider'; +import { QuestionRangeRuleQueryProvider } from './providers/range-rule/question-range-rule-query.provider'; import { QuestionTransition } from './entities/question-transition.entity'; @Module({