feat: add question transition first validations
This commit is contained in:
@@ -12,12 +12,18 @@
|
|||||||
"choice": "Choice",
|
"choice": "Choice",
|
||||||
"score": "Score",
|
"score": "Score",
|
||||||
"weight": "Weight",
|
"weight": "Weight",
|
||||||
"rangeRule": "Range Rule"
|
"rangeRule": "Range Rule",
|
||||||
|
"questionTransition": "Question Transition"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"uniqueOrderConstraint": "Order value should be unique.",
|
"uniqueOrderConstraint": "Order value should be unique.",
|
||||||
"rangeRuleMinMaxConstraint": "min value should be less than max.",
|
"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": {
|
"messages": {
|
||||||
"createSuccess": "Question Created Successfully.",
|
"createSuccess": "Question Created Successfully.",
|
||||||
|
|||||||
@@ -12,12 +12,18 @@
|
|||||||
"choice": "گزینه",
|
"choice": "گزینه",
|
||||||
"score": "نمره",
|
"score": "نمره",
|
||||||
"weight": "وزن",
|
"weight": "وزن",
|
||||||
"rangeRule": "قانون بازه ای"
|
"rangeRule": "قانون بازه ای",
|
||||||
|
"questionTransition": "مسیر سوال"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"uniqueOrderConstraint": "مقدار اولویت تکراری است.",
|
"uniqueOrderConstraint": "مقدار اولویت تکراری است.",
|
||||||
"rangeRuleMinMaxConstraint": "مقدار min باید از max کوچکتر باشد.",
|
"rangeRuleMinMaxConstraint": "مقدار min باید از max کوچکتر باشد.",
|
||||||
"rangeRuleIntersectionConstraint": "بازه {min} تا {max} با دیگر بازه های تعریف شده تداخل دارد."
|
"rangeRuleIntersectionConstraint": "بازه {min} تا {max} با دیگر بازه های تعریف شده تداخل دارد.",
|
||||||
|
"questionTransition": {
|
||||||
|
"sameQuestion": "سوال مبدا و مقصد یکی است.",
|
||||||
|
"fromQuestionNotBelongToTest": "سوال مبدا متعلق به تست نیست.",
|
||||||
|
"toQuestionNotBelongToTest": "سوال مقصد متعلق به تست نیست."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"createSuccess": "سوال با موفقیت ایجاد شد.",
|
"createSuccess": "سوال با موفقیت ایجاد شد.",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Roles } from '@/modules/auth/decorators/roles.decorator';
|
|||||||
import { CreateChoiceDTO } from '@/modules/questions/dtos/create-choice.dto';
|
import { CreateChoiceDTO } from '@/modules/questions/dtos/create-choice.dto';
|
||||||
import { FindChoicesDTO } from '@/modules/questions/dtos/find-choices.dto';
|
import { FindChoicesDTO } from '@/modules/questions/dtos/find-choices.dto';
|
||||||
import { UpdateChoiceDTO } from '@/modules/questions/dtos/update-choice.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 {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { FindQuestionRangeRuleDTO } from '@/modules/questions/dtos/find-question
|
|||||||
import { FindQuestionsDTO } from '@/modules/questions/dtos/find-questions.dto';
|
import { FindQuestionsDTO } from '@/modules/questions/dtos/find-questions.dto';
|
||||||
import { UpdateQuestionRangeRuleDTO } from '@/modules/questions/dtos/update-question-range-rule.dto';
|
import { UpdateQuestionRangeRuleDTO } from '@/modules/questions/dtos/update-question-range-rule.dto';
|
||||||
import { UpdateQuestionDTO } from '@/modules/questions/dtos/update-question.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 { QuestionsService } from '@/modules/questions/providers/questions.service';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ export class QuestionTransition {
|
|||||||
test!: Test;
|
test!: Test;
|
||||||
|
|
||||||
@ManyToOne(() => Question, { nullable: true, onDelete: 'CASCADE' })
|
@ManyToOne(() => Question, { nullable: true, onDelete: 'CASCADE' })
|
||||||
fromQuestion?: Question;
|
fromQuestion!: Question | null;
|
||||||
|
|
||||||
@ManyToOne(() => Question, { nullable: true, onDelete: 'CASCADE' })
|
@ManyToOne(() => Question, { nullable: true, onDelete: 'CASCADE' })
|
||||||
toQuestion?: Question;
|
toQuestion!: Question | null;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: 'enum',
|
type: 'enum',
|
||||||
|
|||||||
@@ -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<Choice>,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject Questions Repository
|
|
||||||
*/
|
|
||||||
@InjectRepository(Question)
|
|
||||||
private readonly questionsRepository: Repository<Question>,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Choice>,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject Questions Repository
|
|
||||||
*/
|
|
||||||
@InjectRepository(Question)
|
|
||||||
private readonly questionsRepository: Repository<Question>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<QuestionRangeRule>,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject Questions Repository
|
|
||||||
*/
|
|
||||||
@InjectRepository(Question)
|
|
||||||
private readonly questionsRepository: Repository<Question>,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<void> {
|
|
||||||
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<QuestionRangeRule>;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<Question>,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inject QuestionRangeRules Repository
|
|
||||||
*/
|
|
||||||
@InjectRepository(QuestionRangeRule)
|
|
||||||
private readonly questionRangeRulesRepository: Repository<QuestionRangeRule>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<QuestionTransition>,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,13 +7,13 @@ import { TestsModule } from '../tests/tests.module';
|
|||||||
import { QuestionsService } from './providers/questions.service';
|
import { QuestionsService } from './providers/questions.service';
|
||||||
import { QuestionsCommandProvider } from './providers/questions-command.provider';
|
import { QuestionsCommandProvider } from './providers/questions-command.provider';
|
||||||
import { QuestionsQueryProvider } from './providers/questions-query.provider';
|
import { QuestionsQueryProvider } from './providers/questions-query.provider';
|
||||||
import { ChoicesCommandProvider } from './providers/choices-command.provider';
|
import { ChoicesCommandProvider } from './providers/choices/choices-command.provider';
|
||||||
import { ChoicesService } from './providers/choices.service';
|
import { ChoicesService } from './providers/choices/choices.service';
|
||||||
import { ChoicesQueryProvider } from './providers/choices-query.provider';
|
import { ChoicesQueryProvider } from './providers/choices/choices-query.provider';
|
||||||
import { QuestionRangeRule } from './entities/question-range-rule.entity';
|
import { QuestionRangeRule } from './entities/question-range-rule.entity';
|
||||||
import { QuestionRangeRuleService } from './providers/question-range-rule.service';
|
import { QuestionRangeRuleService } from './providers/range-rule/question-range-rule.service';
|
||||||
import { QuestionRangeRuleCommandProvider } from './providers/question-range-rule-command.provider';
|
import { QuestionRangeRuleCommandProvider } from './providers/range-rule/question-range-rule-command.provider';
|
||||||
import { QuestionRangeRuleQueryProvider } from './providers/question-range-rule-query.provider';
|
import { QuestionRangeRuleQueryProvider } from './providers/range-rule/question-range-rule-query.provider';
|
||||||
import { QuestionTransition } from './entities/question-transition.entity';
|
import { QuestionTransition } from './entities/question-transition.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
|||||||
Reference in New Issue
Block a user