feat: add question transition constraints and command provider

This commit is contained in:
2026-08-03 17:12:55 +03:30
parent a3b7c70999
commit a3400f66c5
9 changed files with 491 additions and 97 deletions
+8
View File
@@ -1,2 +1,10 @@
export const UNIQUE_TEST_ORDER_IDX = 'UNQ_TEST_ORDER_IDX';
export const UNIQUE_QUESTION_ORDER_IDX = 'UNQ_QUESTION_ORDER_IDX';
export const UNIQUE_QUESTION_NUMBER_RANGE = 'UNQ_QUESTION_NUMBER_RANGE';
export const UNIQUE_QUESTION_CHOICE = 'UNQ_QUESTION_CHOICE';
export const UNIQUE_QUESTION_DEFAULT = 'UNQ_QUESTION_DEFAULT';
export const UNIQUE_QUESTION_END = 'UNQ_QUESTION_END';
export const UNIQUE_TEST_START = 'UNQ_TEST_START';
export const EX_QUESTION_NUMBER_RANGE_OVERLAP =
'EX_QUESTION_NUMBER_RANGE_OVERLAP';
@@ -0,0 +1,148 @@
import { ConflictException, HttpStatus, Injectable } from '@nestjs/common';
import { QueryFailedError, Repository } from 'typeorm';
import { QuestionTransition } from '../../entities/question-transition.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { QuestionTransitionValidatorProvider } from './validations/question-transition-validator.provider';
import { CreateQuestionTransitionDTO } from '../../dtos/create-question-transition.dto';
import {
EX_QUESTION_NUMBER_RANGE_OVERLAP,
UNIQUE_QUESTION_CHOICE,
UNIQUE_QUESTION_DEFAULT,
UNIQUE_QUESTION_END,
UNIQUE_QUESTION_NUMBER_RANGE,
UNIQUE_TEST_START,
} from '../../constants';
import { UpdateQuestionTransitionDTO } from '../../dtos/update-question-transition.dto';
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
import { AppException } from '@/common/exceptions/app.exception';
@Injectable()
export class QuestionTransitionCommandProvider {
constructor(
/**
* Inject QuestionTransitions Repository
*/
@InjectRepository(QuestionTransition)
private readonly transitionsRepository: Repository<QuestionTransition>,
/**
* Inject QuestionTransitionValidator Provider
*/
private readonly questionTransitionValidatorProvider: QuestionTransitionValidatorProvider,
) {}
public async createOne(
createQuestionTransitionDto: CreateQuestionTransitionDTO,
) {
const { test, fromQuestion, toQuestion, type, conditionValue } =
await this.questionTransitionValidatorProvider.validate(
createQuestionTransitionDto,
);
const newTransition = this.transitionsRepository.create({
test,
fromQuestion,
toQuestion,
type,
condition: conditionValue,
});
try {
await this.transitionsRepository.save(newTransition);
} catch (err) {
this.handleDBExceptions(err);
}
}
public async updateOne(
id: number,
updateQuestionTransitionDto: UpdateQuestionTransitionDTO,
) {
const { transition, test, fromQuestion, toQuestion, type, conditionValue } =
await this.questionTransitionValidatorProvider.validate(
updateQuestionTransitionDto,
id,
);
if (!transition) {
throw new AppException(
'common.errors.entityNotFound',
HttpStatus.NOT_FOUND,
{ entity: '$t(questions.fields.questionTransition)' },
);
}
transition.test = test;
transition.fromQuestion = fromQuestion;
transition.toQuestion = toQuestion;
transition.type = type;
transition.condition = conditionValue;
try {
await this.transitionsRepository.save(transition);
} catch (err) {
this.handleDBExceptions(err);
}
}
private handleDBExceptions(err: unknown) {
if (err instanceof QueryFailedError) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const constraint = (err as any).constraint;
switch (constraint) {
case UNIQUE_TEST_START:
throw new ConflictException(
'questions.errors.questionTransition.startAlreadyExists',
);
case UNIQUE_QUESTION_END:
throw new ConflictException(
'questions.errors.questionTransition.endAlreadyExists',
);
case UNIQUE_QUESTION_DEFAULT:
throw new ConflictException(
'questions.errors.questionTransition.defaultAlreadyExists',
);
case UNIQUE_QUESTION_CHOICE:
case UNIQUE_QUESTION_NUMBER_RANGE:
throw new ConflictException(
'questions.errors.questionTransition.duplicateTransition',
);
case EX_QUESTION_NUMBER_RANGE_OVERLAP:
throw new ConflictException(
'questions.errors.questionTransition.numberRangeOverlap',
);
}
}
throw new AppTimeoutException(err);
}
public async deleteOne(id: number) {
let transition: QuestionTransition | null = null;
try {
transition = await this.transitionsRepository.findOneBy({ id });
} catch (err) {
throw new AppTimeoutException(err);
}
if (!transition) {
throw new AppException(
'common.errors.entityNotFound',
HttpStatus.NOT_FOUND,
{ entity: '$t(questions.fields.questionTransition)' },
);
}
try {
await this.transitionsRepository.delete(transition.id);
} catch (err) {
throw new AppTimeoutException(err);
}
}
}
@@ -14,6 +14,7 @@ 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';
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
@Injectable()
export class QuestionTransitionValidatorProvider {
@@ -50,15 +51,21 @@ export class QuestionTransitionValidatorProvider {
let toQuestion: Question | null;
let conditionValue: TransitionConditionType | undefined;
let transition: QuestionTransition | null = null;
if (transitionId !== undefined) {
const transition = await this.transitionsRepository.findOne({
where: { id: transitionId },
relations: {
test: true,
fromQuestion: true,
toQuestion: true,
},
});
try {
transition = await this.transitionsRepository.findOne({
where: { id: transitionId },
relations: {
test: true,
fromQuestion: true,
toQuestion: true,
},
});
} catch (err) {
throw new AppTimeoutException(err);
}
if (!transition) {
throw new AppException(
@@ -75,7 +82,7 @@ export class QuestionTransitionValidatorProvider {
type = dto.type ?? transition.type;
if (dto.fromQuestion) {
if (dto.fromQuestion !== undefined && dto.fromQuestion !== null) {
fromQuestion = await this.questionsQueryProvider.findOneByIdForAdmin(
dto.fromQuestion,
);
@@ -85,7 +92,7 @@ export class QuestionTransitionValidatorProvider {
fromQuestion = transition.fromQuestion;
}
if (dto.toQuestion) {
if (dto.toQuestion !== undefined && dto.toQuestion !== null) {
toQuestion = await this.questionsQueryProvider.findOneByIdForAdmin(
dto.toQuestion,
);
@@ -138,7 +145,7 @@ export class QuestionTransitionValidatorProvider {
await this.checkTransitionDBProvider.validate(options, transitionId);
return options;
return { ...options, transition };
}
private validateQuestions(