feat: add choices entity admin crud
This commit is contained in:
@@ -1 +1,2 @@
|
||||
export const UNIQUE_TEST_ORDER_IDX = 'UNQ_TEST_ORDER_IDX';
|
||||
export const UNIQUE_QUESTION_ORDER_IDX = 'UNQ_QUESTION_ORDER_IDX';
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { IsNotEmpty, IsNumber, IsPositive, MaxLength } from 'class-validator';
|
||||
import { i18nValidationMessage as t } from 'nestjs-i18n';
|
||||
|
||||
export class CreateChoiceDTO {
|
||||
@IsPositive({
|
||||
message: t('validation.wrongField', {
|
||||
field: '$t(questions.fields.question)',
|
||||
}),
|
||||
})
|
||||
@IsNotEmpty({
|
||||
message: t('validation.requiredField', {
|
||||
field: '$t(questions.fields.question)',
|
||||
}),
|
||||
})
|
||||
question!: number;
|
||||
|
||||
@IsNotEmpty({
|
||||
message: t('validation.requiredField', {
|
||||
field: '$t(questions.fields.text)',
|
||||
}),
|
||||
})
|
||||
@MaxLength(255, {
|
||||
message: t('validation.maxLengthField', {
|
||||
field: '$t(users.fields.text)',
|
||||
max: 255,
|
||||
}),
|
||||
})
|
||||
text!: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsNotEmpty({
|
||||
message: t('validation.requiredField', {
|
||||
field: '$t(questions.fields.value)',
|
||||
}),
|
||||
})
|
||||
value!: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsNotEmpty({
|
||||
message: t('validation.requiredField', {
|
||||
field: '$t(questions.fields.order)',
|
||||
}),
|
||||
})
|
||||
order!: number;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PaginationQueryDTO } from '@/common/dtos/pagination-query.dto';
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNumber, IsOptional, IsString } from 'class-validator';
|
||||
import { ChoiceSortField } from '../enums/choice-sort-field.enum';
|
||||
|
||||
export class FindChoicesDTO extends PartialType(PaginationQueryDTO) {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
id?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
question?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
text?: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
value?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
choiceOrder?: number;
|
||||
|
||||
@IsEnum(ChoiceSortField)
|
||||
@IsOptional()
|
||||
sortBy?: ChoiceSortField;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateChoiceDTO } from './create-choice.dto';
|
||||
|
||||
export class UpdateChoiceDTO extends PartialType(CreateChoiceDTO) {}
|
||||
@@ -1,7 +1,15 @@
|
||||
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
} from 'typeorm';
|
||||
import { Question } from './question.entity';
|
||||
import { UNIQUE_QUESTION_ORDER_IDX } from '../constants';
|
||||
|
||||
@Entity()
|
||||
@Index(UNIQUE_QUESTION_ORDER_IDX, ['question', 'order'], { unique: true })
|
||||
export class Choice {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum ChoiceSortField {
|
||||
ID = 'id',
|
||||
QUESTION = 'question',
|
||||
TEXT = 'text',
|
||||
ORDER = 'order',
|
||||
VALUE = 'value',
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { FindOptionsWhere, ILike } from 'typeorm';
|
||||
import { FindChoicesDTO } from '../dtos/find-choices.dto';
|
||||
import { Choice } from '../entities/choice.entity';
|
||||
|
||||
export const filterChoices = (query: FindChoicesDTO) => {
|
||||
const where: FindOptionsWhere<Choice> = {};
|
||||
|
||||
if (query.id) {
|
||||
where.id = query.id;
|
||||
}
|
||||
|
||||
if (query.text) {
|
||||
where.text = ILike(`%${query.text}%`);
|
||||
}
|
||||
|
||||
if (query.choiceOrder !== undefined) {
|
||||
where.order = query.choiceOrder;
|
||||
}
|
||||
|
||||
if (query.value !== undefined) {
|
||||
where.value = query.value;
|
||||
}
|
||||
|
||||
return where;
|
||||
};
|
||||
@@ -0,0 +1,149 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { ConflictException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Question } from '../entities/question.entity';
|
||||
import { QueryFailedError, Repository } from 'typeorm';
|
||||
import { TestsService } from '@/modules/tests/providers/tests.service';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { CreateQuestionDTO } from '../dtos/create-question.dto';
|
||||
import { AppResponse } from '@/common/responses';
|
||||
import { UNIQUE_TEST_ORDER_IDX } from '../constants';
|
||||
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
|
||||
import { UpdateQuestionDTO } from '../dtos/update-question.dto';
|
||||
import { validateMetadata } from '../helpers/validate-metadata.helper';
|
||||
import { AppException } from '@/common/exceptions/app.exception';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionsCommandProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Inject Questions Repository
|
||||
*/
|
||||
@InjectRepository(Question)
|
||||
private readonly questionsRepository: Repository<Question>,
|
||||
|
||||
/**
|
||||
* Inject Tests Service
|
||||
*/
|
||||
private readonly testsService: TestsService,
|
||||
|
||||
/**
|
||||
* Inject I18n Service
|
||||
*/
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
public async createOne(createQuestionDto: CreateQuestionDTO) {
|
||||
const test = await this.testsService.adminGetOne(createQuestionDto.test);
|
||||
|
||||
if (!test) {
|
||||
throw new AppException(
|
||||
'common.errors.entityNotFound',
|
||||
HttpStatus.NOT_FOUND,
|
||||
{ entity: '$t(tests.fields.test)' },
|
||||
);
|
||||
}
|
||||
|
||||
const newQuestion = this.questionsRepository.create({
|
||||
...createQuestionDto,
|
||||
test,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.questionsRepository.save(newQuestion);
|
||||
return new AppResponse(
|
||||
newQuestion,
|
||||
this.i18nService.translate('common.messages.createSuccess', {
|
||||
args: { entity: '$t(questions.fields.question)' },
|
||||
}),
|
||||
);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof QueryFailedError &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(err as any).constraint === UNIQUE_TEST_ORDER_IDX
|
||||
) {
|
||||
throw new ConflictException('questions.errors.uniqueOrderConstraint');
|
||||
}
|
||||
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
}
|
||||
|
||||
public async updateOne(id: number, updateQuestionDto: UpdateQuestionDTO) {
|
||||
const question = await this.questionsRepository.findOneBy({ id });
|
||||
|
||||
if (!question) {
|
||||
throw new AppException(
|
||||
'common.errors.entityNotFound',
|
||||
HttpStatus.NOT_FOUND,
|
||||
{ entity: '$t(questions.fields.question)' },
|
||||
);
|
||||
}
|
||||
|
||||
if (updateQuestionDto.test) {
|
||||
const test = await this.testsService.adminGetOne(updateQuestionDto.test);
|
||||
|
||||
if (!test) {
|
||||
throw new AppException(
|
||||
'common.errors.entityNotFound',
|
||||
HttpStatus.NOT_FOUND,
|
||||
{ entity: '$t(tests.fields.test)' },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const type =
|
||||
updateQuestionDto.type !== undefined
|
||||
? updateQuestionDto.type
|
||||
: question.type;
|
||||
|
||||
const metadata =
|
||||
updateQuestionDto.metadata !== undefined
|
||||
? updateQuestionDto.metadata
|
||||
: question.metadata;
|
||||
|
||||
const errors = await validateMetadata(type, metadata);
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
throw new AppException(
|
||||
'validation.wrongFieldFormat',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
{ field: '$t(questions.fields.metadata)' },
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(question, updateQuestionDto);
|
||||
|
||||
try {
|
||||
return await this.questionsRepository.save(question);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof QueryFailedError &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(err as any).constraint === UNIQUE_TEST_ORDER_IDX
|
||||
) {
|
||||
throw new ConflictException('questions.errors.uniqueOrderConstraint');
|
||||
}
|
||||
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteOne(id: number) {
|
||||
let question: Question | null = null;
|
||||
|
||||
try {
|
||||
question = await this.questionsRepository.findOneBy({ id });
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
|
||||
if (!question)
|
||||
throw new AppException(
|
||||
'common.errors.entityNotFound',
|
||||
HttpStatus.NOT_FOUND,
|
||||
{ entity: '$t(questions.fields.question)' },
|
||||
);
|
||||
|
||||
try {
|
||||
await this.questionsRepository.delete(question.id);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { filterQuestions } from '../helpers/filter-questions.helper';
|
||||
import Utils from '@/common/utils';
|
||||
import { FindQuestionsDTO } from '../dtos/find-questions.dto';
|
||||
import { Test } from '@nestjs/testing';
|
||||
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 { TestsService } from '@/modules/tests/providers/tests.service';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionsQueryProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Inject Questions Repository
|
||||
*/
|
||||
@InjectRepository(Question)
|
||||
private readonly questionsRepository: Repository<Question>,
|
||||
|
||||
/**
|
||||
* Inject Tests Service
|
||||
*/
|
||||
private readonly testsService: TestsService,
|
||||
) {}
|
||||
|
||||
public async findAllForAdmin(query: FindQuestionsDTO) {
|
||||
const filters = filterQuestions(query);
|
||||
const { limit, page, skip, take } =
|
||||
Utils.PaginationUtils.getPaginationOptions(query);
|
||||
|
||||
let selectedTest: Test | null = null;
|
||||
|
||||
if (query.test) {
|
||||
try {
|
||||
selectedTest = await this.testsService.adminGetOne(query.test);
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const [questions, total] = await this.questionsRepository.findAndCount({
|
||||
where: {
|
||||
...filters,
|
||||
...(selectedTest && {
|
||||
test: selectedTest,
|
||||
}),
|
||||
},
|
||||
loadRelationIds: true,
|
||||
skip,
|
||||
take,
|
||||
order: query.sortBy
|
||||
? {
|
||||
[query.sortBy]: query.order ?? 'ASC',
|
||||
}
|
||||
: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return new PaginatedResponse(questions, {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
}
|
||||
|
||||
public async findOneForAdmin(id: number) {
|
||||
try {
|
||||
return await this.questionsRepository.findOne({
|
||||
where: { id },
|
||||
relations: { test: true },
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,194 +1,41 @@
|
||||
import {
|
||||
ConflictException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { QueryFailedError, Repository } from 'typeorm';
|
||||
import { Question } from '../entities/question.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateQuestionDTO } from '../dtos/create-question.dto';
|
||||
import { TestsService } from '@/modules/tests/providers/tests.service';
|
||||
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
|
||||
import { AppResponse, PaginatedResponse } from '@/common/responses';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { UNIQUE_TEST_ORDER_IDX } from '../constants';
|
||||
import { FindQuestionsDTO } from '../dtos/find-questions.dto';
|
||||
import { filterQuestions } from '../helpers/filter-questions.helper';
|
||||
import Utils from '@/common/utils';
|
||||
import { Test } from '@/modules/tests/entities/test.entity';
|
||||
import { UpdateQuestionDTO } from '../dtos/update-question.dto';
|
||||
import { validateMetadata } from '../helpers/validate-metadata.helper';
|
||||
import { AppException } from '@/common/exceptions/app.exception';
|
||||
import { QuestionsCommandProvider } from './questions-command.provider';
|
||||
import { QuestionsQueryProvider } from './questions-query.provider';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionsService {
|
||||
constructor(
|
||||
/**
|
||||
* Inject Questions Repository
|
||||
* Inject Questions Command Provider
|
||||
*/
|
||||
@InjectRepository(Question)
|
||||
private readonly questionsRepository: Repository<Question>,
|
||||
private readonly questionsCommandProvider: QuestionsCommandProvider,
|
||||
|
||||
/**
|
||||
* Inject Tests Service
|
||||
* Inject Questions Query Provider
|
||||
*/
|
||||
private readonly testsService: TestsService,
|
||||
|
||||
/**
|
||||
* Inject I18n Service
|
||||
*/
|
||||
private readonly i18nService: I18nService,
|
||||
private readonly questionsQueryProvider: QuestionsQueryProvider,
|
||||
) {}
|
||||
|
||||
public async createOne(createQuestionDto: CreateQuestionDTO) {
|
||||
const test = await this.testsService.adminGetOne(createQuestionDto.test);
|
||||
|
||||
if (!test) {
|
||||
throw new NotFoundException('tests.errors.testNotFound');
|
||||
}
|
||||
|
||||
const newQuestion = this.questionsRepository.create({
|
||||
...createQuestionDto,
|
||||
test,
|
||||
});
|
||||
|
||||
try {
|
||||
await this.questionsRepository.save(newQuestion);
|
||||
return new AppResponse(
|
||||
newQuestion,
|
||||
this.i18nService.translate('questions.messages.createSuccess'),
|
||||
);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof QueryFailedError &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(err as any).constraint === UNIQUE_TEST_ORDER_IDX
|
||||
) {
|
||||
throw new ConflictException('questions.errors.uniqueOrderConstraint');
|
||||
}
|
||||
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
return await this.questionsCommandProvider.createOne(createQuestionDto);
|
||||
}
|
||||
|
||||
public async findAllForAdmin(query: FindQuestionsDTO) {
|
||||
const filters = filterQuestions(query);
|
||||
const { limit, page, skip, take } =
|
||||
Utils.PaginationUtils.getPaginationOptions(query);
|
||||
|
||||
let selectedTest: Test | null = null;
|
||||
|
||||
if (query.test) {
|
||||
try {
|
||||
selectedTest = await this.testsService.adminGetOne(query.test);
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const [questions, total] = await this.questionsRepository.findAndCount({
|
||||
where: {
|
||||
...filters,
|
||||
...(selectedTest && {
|
||||
test: selectedTest,
|
||||
}),
|
||||
},
|
||||
loadRelationIds: true,
|
||||
skip,
|
||||
take,
|
||||
order: query.sortBy
|
||||
? {
|
||||
[query.sortBy]: query.order ?? 'ASC',
|
||||
}
|
||||
: { createdAt: 'DESC' },
|
||||
});
|
||||
|
||||
return new PaginatedResponse(questions, {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
return await this.questionsQueryProvider.findAllForAdmin(query);
|
||||
}
|
||||
|
||||
public async findOneForAdmin(id: number) {
|
||||
try {
|
||||
return await this.questionsRepository.findOne({
|
||||
where: { id },
|
||||
relations: { test: true },
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
return await this.questionsQueryProvider.findOneForAdmin(id);
|
||||
}
|
||||
|
||||
public async updateOne(id: number, updateQuestionDto: UpdateQuestionDTO) {
|
||||
const question = await this.questionsRepository.findOneBy({ id });
|
||||
|
||||
if (!question) {
|
||||
throw new NotFoundException('questions.errors.questionNotFound');
|
||||
}
|
||||
|
||||
const type =
|
||||
updateQuestionDto.type !== undefined
|
||||
? updateQuestionDto.type
|
||||
: question.type;
|
||||
|
||||
const metadata =
|
||||
updateQuestionDto.metadata !== undefined
|
||||
? updateQuestionDto.metadata
|
||||
: question.metadata;
|
||||
|
||||
const errors = await validateMetadata(type, metadata);
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
throw new AppException(
|
||||
'validation.wrongFieldFormat',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
{ field: '$t(questions.fields.metadata)' },
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(question, updateQuestionDto);
|
||||
|
||||
console.log(question);
|
||||
|
||||
try {
|
||||
return await this.questionsRepository.save(question);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof QueryFailedError &&
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
(err as any).constraint === UNIQUE_TEST_ORDER_IDX
|
||||
) {
|
||||
throw new ConflictException('questions.errors.uniqueOrderConstraint');
|
||||
}
|
||||
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
return await this.questionsCommandProvider.updateOne(id, updateQuestionDto);
|
||||
}
|
||||
|
||||
public async deleteOne(id: number) {
|
||||
let question: Question | null = null;
|
||||
|
||||
try {
|
||||
question = await this.questionsRepository.findOneBy({ id });
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
|
||||
if (!question)
|
||||
throw new NotFoundException('questions.errors.questionNotFound');
|
||||
|
||||
try {
|
||||
await this.questionsRepository.delete(question.id);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
return await this.questionsCommandProvider.deleteOne(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,23 @@ import { Choice } from './entities/choice.entity';
|
||||
import { QuestionsController } from './questions.controller';
|
||||
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';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Question, Choice]), TestsModule],
|
||||
controllers: [QuestionsController],
|
||||
providers: [QuestionsService],
|
||||
exports: [QuestionsService],
|
||||
providers: [
|
||||
QuestionsCommandProvider,
|
||||
QuestionsQueryProvider,
|
||||
QuestionsService,
|
||||
ChoicesCommandProvider,
|
||||
ChoicesQueryProvider,
|
||||
ChoicesService,
|
||||
],
|
||||
exports: [QuestionsService, ChoicesService],
|
||||
})
|
||||
export class QuestionsModule {}
|
||||
|
||||
Reference in New Issue
Block a user