Complete question transition validation
This commit is contained in:
@@ -22,7 +22,24 @@
|
||||
"questionTransition": {
|
||||
"sameQuestion": "fromQuestion and toQuestion are the same",
|
||||
"fromQuestionNotBelongToTest": "fromQuestion doesnt belong to the test.",
|
||||
"toQuestionNotBelongToTest": "toQuestion doesnt belong to the test."
|
||||
"toQuestionNotBelongToTest": "toQuestion doesnt belong to the test.",
|
||||
"startMustNotHaveFromQuestion": "Start must not have fromQuestion.",
|
||||
"startMustHaveToQuestion": "Start must have toQuestion.",
|
||||
"startMustNotHaveCondition": "Start must not have condition.",
|
||||
"endMustHaveFromQuestion": "End must have fromQuestion.",
|
||||
"endMustNotHaveToQuestion": "End must not have toQuestion.",
|
||||
"endMustNotHaveCondition": "End must not have condition.",
|
||||
"invalidTransition": "Invalid Transition!",
|
||||
"defaultMustNotHaveCondition": "Default must not have condition.",
|
||||
"fromQuestionMustBeMultiChoice": "fromQuestion must be multi choice.",
|
||||
"invalidChoiceCondition": "Invalid choice condition!",
|
||||
"choiceNotBelongToQuestion": "Choice does not belong to the question.",
|
||||
"fromQuestionMustBeNumeric": "fromQuestion must be numeric.",
|
||||
"invalidNumberRange": "Invalid number range!",
|
||||
"startAlreadyExists": "Start already exists.",
|
||||
"endAlreadyExists": "End already exists.",
|
||||
"defaultAlreadyExists": "Default already exists.",
|
||||
"duplicateTransition": "Duplicate Transition!"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
|
||||
@@ -22,7 +22,24 @@
|
||||
"questionTransition": {
|
||||
"sameQuestion": "سوال مبدا و مقصد یکی است.",
|
||||
"fromQuestionNotBelongToTest": "سوال مبدا متعلق به تست نیست.",
|
||||
"toQuestionNotBelongToTest": "سوال مقصد متعلق به تست نیست."
|
||||
"toQuestionNotBelongToTest": "سوال مقصد متعلق به تست نیست.",
|
||||
"startMustNotHaveFromQuestion": "حالت شروع نباید سوال مبدا داشته باشد.",
|
||||
"startMustHaveToQuestion": "حالت شروع باید سوال مقصد داشته باشد.",
|
||||
"startMustNotHaveCondition": "حالت شروع نباید شرط داشته باشد.",
|
||||
"endMustHaveFromQuestion": "حالت پایان باید سوال مقصد داشته باشد.",
|
||||
"endMustNotHaveToQuestion": "حالت پایان نباید سوال مبدا داشته باشد.",
|
||||
"endMustNotHaveCondition": "حالت پایان نباید شرط داشته باشد.",
|
||||
"invalidTransition": "مسیر اشتباه است!",
|
||||
"defaultMustNotHaveCondition": "حالت پیشفرض نباید شرط داشته باشد.",
|
||||
"fromQuestionMustBeMultiChoice": "سوال مبدا باید چند گزینه ای باشد.",
|
||||
"invalidChoiceCondition": "شرط گزینه اشتباه است!",
|
||||
"choiceNotBelongToQuestion": "گزینه متعلق به سوال نیست.",
|
||||
"fromQuestionMustBeNumeric": "سوال مبدا باید از نوع عددی باشد.",
|
||||
"invalidNumberRange": "بازه عددی اشتباه است!",
|
||||
"startAlreadyExists": "حالت شروع وجود دارد.",
|
||||
"endAlreadyExists": "حالت پایان وجود دارد.",
|
||||
"defaultAlreadyExists": "حالت پیشفرض وجود دارد.",
|
||||
"duplicateTransition": "مسیر تکراری است!"
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
import { AppException } from '@/common/exceptions/app.exception';
|
||||
import { QuestionTransition } from '@/modules/questions/entities/question-transition.entity';
|
||||
import { QuestionTransitionType } from '@/modules/questions/enums/question-transition-type.enum';
|
||||
import { NumericCondition } from '@/modules/questions/interfaces/numeric-condition.interface';
|
||||
import { SingleChoiceCondition } from '@/modules/questions/interfaces/single-choice-condition.interface';
|
||||
import { TransitionValidateOptions } from '@/modules/questions/interfaces/transition-validate-options.interface';
|
||||
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import type { Repository } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
export class CheckTransitionDBProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Inject Transitions Repository
|
||||
*/
|
||||
@InjectRepository(QuestionTransition)
|
||||
private readonly transitionsRepository: Repository<QuestionTransition>,
|
||||
) {}
|
||||
|
||||
public async validate(
|
||||
options: TransitionValidateOptions,
|
||||
transitionId?: number,
|
||||
) {
|
||||
await this.validateSingleStart(options, transitionId);
|
||||
|
||||
await this.validateSingleEnd(options, transitionId);
|
||||
|
||||
await this.validateDefaultTransition(options, transitionId);
|
||||
|
||||
await this.validateDuplicateCondition(options, transitionId);
|
||||
}
|
||||
|
||||
private async validateSingleStart(
|
||||
options: TransitionValidateOptions,
|
||||
transitionId?: number,
|
||||
) {
|
||||
if (options.type !== QuestionTransitionType.START) return;
|
||||
|
||||
const qb = this.transitionsRepository
|
||||
.createQueryBuilder('t')
|
||||
.where('t.testId = :testId', {
|
||||
testId: options.test.id,
|
||||
})
|
||||
.andWhere('t.type = :type', {
|
||||
type: QuestionTransitionType.START,
|
||||
});
|
||||
|
||||
if (transitionId) {
|
||||
qb.andWhere('t.id != :transitionId', {
|
||||
transitionId,
|
||||
});
|
||||
}
|
||||
|
||||
if (await qb.getExists()) {
|
||||
throw new AppException(
|
||||
'questions.errors.questionTransition.startAlreadyExists',
|
||||
HttpStatus.CONFLICT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async validateSingleEnd(
|
||||
options: TransitionValidateOptions,
|
||||
transitionId?: number,
|
||||
) {
|
||||
if (options.type !== QuestionTransitionType.END) return;
|
||||
|
||||
const qb = this.transitionsRepository
|
||||
.createQueryBuilder('t')
|
||||
.where('t.fromQuestionId = :fromQuestionId', {
|
||||
fromQuestionId: options.fromQuestion?.id,
|
||||
})
|
||||
.andWhere('t.type = :type', {
|
||||
type: QuestionTransitionType.END,
|
||||
});
|
||||
|
||||
if (transitionId) {
|
||||
qb.andWhere('t.id != :transitionId', {
|
||||
transitionId,
|
||||
});
|
||||
}
|
||||
|
||||
if (await qb.getExists()) {
|
||||
throw new AppException(
|
||||
'questions.errors.questionTransition.endAlreadyExists',
|
||||
HttpStatus.CONFLICT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async validateDefaultTransition(
|
||||
options: TransitionValidateOptions,
|
||||
transitionId?: number,
|
||||
) {
|
||||
if (options.type !== QuestionTransitionType.DEFAULT) return;
|
||||
|
||||
const qb = this.transitionsRepository
|
||||
.createQueryBuilder('t')
|
||||
.where('t.fromQuestionId = :fromQuestionId', {
|
||||
fromQuestionId: options.fromQuestion?.id,
|
||||
})
|
||||
.andWhere('t.type = :type', {
|
||||
type: QuestionTransitionType.DEFAULT,
|
||||
});
|
||||
|
||||
if (transitionId) {
|
||||
qb.andWhere('t.id != :transitionId', {
|
||||
transitionId,
|
||||
});
|
||||
}
|
||||
|
||||
if (await qb.getExists()) {
|
||||
throw new AppException(
|
||||
'questions.errors.questionTransition.defaultAlreadyExists',
|
||||
HttpStatus.CONFLICT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async validateDuplicateCondition(
|
||||
options: TransitionValidateOptions,
|
||||
transitionId?: number,
|
||||
) {
|
||||
if (
|
||||
options.type === QuestionTransitionType.DEFAULT ||
|
||||
options.type === QuestionTransitionType.START ||
|
||||
options.type === QuestionTransitionType.END
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const qb = this.transitionsRepository
|
||||
.createQueryBuilder('t')
|
||||
.where('t.fromQuestionId = :fromQuestionId', {
|
||||
fromQuestionId: options.fromQuestion?.id,
|
||||
})
|
||||
.andWhere('t.type = :type', {
|
||||
type: options.type,
|
||||
});
|
||||
|
||||
if (options.type === QuestionTransitionType.CHOICE) {
|
||||
qb.andWhere(`t.conditionValue ->> 'choiceId' = :choiceId`, {
|
||||
choiceId: String(
|
||||
(options.conditionValue as SingleChoiceCondition).choiceId,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (options.type === QuestionTransitionType.NUMBER_RANGE) {
|
||||
qb.andWhere(`t.conditionValue ->> 'min' = :min`, {
|
||||
min: String((options.conditionValue as NumericCondition).min),
|
||||
}).andWhere(`t.conditionValue ->> 'max' = :max`, {
|
||||
max: String((options.conditionValue as NumericCondition).max),
|
||||
});
|
||||
}
|
||||
|
||||
if (transitionId) {
|
||||
qb.andWhere('t.id != :transitionId', {
|
||||
transitionId,
|
||||
});
|
||||
}
|
||||
|
||||
if (await qb.getExists()) {
|
||||
throw new AppException(
|
||||
'questions.errors.questionTransition.duplicateTransition',
|
||||
HttpStatus.CONFLICT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+20
-1
@@ -1,6 +1,6 @@
|
||||
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import type { Repository } from 'typeorm';
|
||||
import { TestsService } from '@/modules/tests/providers/tests.service';
|
||||
import { QuestionTransition } from '../../../entities/question-transition.entity';
|
||||
import { Question } from '@/modules/questions/entities/question.entity';
|
||||
@@ -13,16 +13,31 @@ import { AppException } from '@/common/exceptions/app.exception';
|
||||
import { QuestionsQueryProvider } from '../../questions-query.provider';
|
||||
import { TransitionValidateOptions } from '../../../interfaces/transition-validate-options.interface';
|
||||
import { QuestionTransitionTypeValidator } from '../../../validators/question-transition-type.validator';
|
||||
import { CheckTransitionDBProvider } from './check-transition-db.provider';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionTransitionValidatorProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Inject Transitions Repository
|
||||
*/
|
||||
@InjectRepository(QuestionTransition)
|
||||
private readonly transitionsRepository: Repository<QuestionTransition>,
|
||||
|
||||
/**
|
||||
* Inject Tests Service
|
||||
*/
|
||||
private readonly testsService: TestsService,
|
||||
|
||||
/**
|
||||
* Inject Questions Query Provider
|
||||
*/
|
||||
private readonly questionsQueryProvider: QuestionsQueryProvider,
|
||||
|
||||
/**
|
||||
* Inject CheckTransitionDB Provider
|
||||
*/
|
||||
private readonly checkTransitionDBProvider: CheckTransitionDBProvider,
|
||||
) {}
|
||||
|
||||
public async validate(
|
||||
@@ -120,6 +135,10 @@ export class QuestionTransitionValidatorProvider {
|
||||
};
|
||||
|
||||
QuestionTransitionTypeValidator.validateTransitionType(options);
|
||||
|
||||
await this.checkTransitionDBProvider.validate(options, transitionId);
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private validateQuestions(
|
||||
|
||||
@@ -15,6 +15,8 @@ import { QuestionRangeRuleService } from './providers/range-rule/question-range-
|
||||
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';
|
||||
import { CheckTransitionDBProvider } from './providers/transition/validations/check-transition-db.provider';
|
||||
import { QuestionTransitionValidatorProvider } from './providers/transition/validations/question-transition-validator.provider';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -28,15 +30,24 @@ import { QuestionTransition } from './entities/question-transition.entity';
|
||||
],
|
||||
controllers: [QuestionsController],
|
||||
providers: [
|
||||
// Questsions
|
||||
QuestionsCommandProvider,
|
||||
QuestionsQueryProvider,
|
||||
QuestionsService,
|
||||
|
||||
// Choices
|
||||
ChoicesCommandProvider,
|
||||
ChoicesQueryProvider,
|
||||
ChoicesService,
|
||||
|
||||
// Question Range Rules
|
||||
QuestionRangeRuleCommandProvider,
|
||||
QuestionRangeRuleQueryProvider,
|
||||
QuestionRangeRuleService,
|
||||
|
||||
// Question Transitions
|
||||
CheckTransitionDBProvider,
|
||||
QuestionTransitionValidatorProvider,
|
||||
],
|
||||
exports: [QuestionsService, ChoicesService, QuestionRangeRuleService],
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import { NumericCondition } from '@/modules/questions/interfaces/numeric-conditi
|
||||
import { SingleChoiceCondition } from '@/modules/questions/interfaces/single-choice-condition.interface';
|
||||
import { TransitionValidateOptions } from '@/modules/questions/interfaces/transition-validate-options.interface';
|
||||
import { HttpStatus } from '@nestjs/common';
|
||||
import { NumericMetadata } from '../interfaces/numeric-metadata.interface';
|
||||
|
||||
export class QuestionTransitionTypeValidator {
|
||||
public static validateTransitionType(options: TransitionValidateOptions) {
|
||||
@@ -34,21 +35,21 @@ export class QuestionTransitionTypeValidator {
|
||||
private static validateStart(options: TransitionValidateOptions) {
|
||||
if (options.fromQuestion) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.startMustNotHaveFromQuestion',
|
||||
'questions.errors.questionTransition.startMustNotHaveFromQuestion',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (!options.toQuestion) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.startMustHaveToQuestion',
|
||||
'questions.errors.questionTransition.startMustHaveToQuestion',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.conditionValue) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.startMustNotHaveCondition',
|
||||
'questions.errors.questionTransition.startMustNotHaveCondition',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
@@ -57,21 +58,21 @@ export class QuestionTransitionTypeValidator {
|
||||
private static validateEnd(options: TransitionValidateOptions) {
|
||||
if (!options.fromQuestion) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.endMustHaveFromQuestion',
|
||||
'questions.errors.questionTransition.endMustHaveFromQuestion',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.toQuestion) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.endMustNotHaveToQuestion',
|
||||
'questions.errors.questionTransition.endMustNotHaveToQuestion',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.conditionValue) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.endMustNotHaveCondition',
|
||||
'questions.errors.questionTransition.endMustNotHaveCondition',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
@@ -80,14 +81,14 @@ export class QuestionTransitionTypeValidator {
|
||||
private static validateDefault(options: TransitionValidateOptions) {
|
||||
if (!options.fromQuestion || !options.toQuestion) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.invalidTransition',
|
||||
'questions.errors.questionTransition.invalidTransition',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (options.conditionValue) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.defaultMustNotHaveCondition',
|
||||
'questions.errors.questionTransition.defaultMustNotHaveCondition',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
@@ -98,14 +99,14 @@ export class QuestionTransitionTypeValidator {
|
||||
|
||||
if (!fromQuestion || !toQuestion) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.invalidTransition',
|
||||
'questions.errors.questionTransition.invalidTransition',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (fromQuestion.type !== QuestionType.MULTI_CHOICE) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.fromQuestionMustBeMultiChoice',
|
||||
'questions.errors.questionTransition.fromQuestionMustBeMultiChoice',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
@@ -114,7 +115,7 @@ export class QuestionTransitionTypeValidator {
|
||||
|
||||
if (typeof choiceId !== 'number') {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.invalidChoiceCondition',
|
||||
'questions.errors.questionTransition.invalidChoiceCondition',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
@@ -125,7 +126,7 @@ export class QuestionTransitionTypeValidator {
|
||||
|
||||
if (!choiceExists) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.choiceNotBelongToQuestion',
|
||||
'questions.errors.questionTransition.choiceNotBelongToQuestion',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
@@ -138,14 +139,14 @@ export class QuestionTransitionTypeValidator {
|
||||
|
||||
if (!fromQuestion || !toQuestion) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.invalidTransition',
|
||||
'questions.errors.questionTransition.invalidTransition',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (fromQuestion.type !== QuestionType.NUMERIC) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.fromQuestionMustBeNumeric',
|
||||
'questions.errors.questionTransition.fromQuestionMustBeNumeric',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
@@ -158,16 +159,27 @@ export class QuestionTransitionTypeValidator {
|
||||
typeof condition.max !== 'number'
|
||||
) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.invalidNumberRange',
|
||||
'questions.errors.questionTransition.invalidNumberRange',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (condition.min > condition.max) {
|
||||
throw new AppException(
|
||||
'questionTransitions.errors.invalidNumberRange',
|
||||
'questions.errors.questionTransition.invalidNumberRange',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (fromQuestion.metadata) {
|
||||
const metadata = fromQuestion.metadata as NumericMetadata;
|
||||
|
||||
if (condition.min < metadata.min || condition.max > metadata.max) {
|
||||
throw new AppException(
|
||||
'questions.errors.questionTransition.invalidNumberRange',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user