feat: complete question transitions admin routes
This commit is contained in:
+8
-7
@@ -21,8 +21,8 @@ import { QuestionsModule } from './modules/questions/questions.module';
|
||||
import { SubmissionsModule } from './modules/submissions/submissions.module';
|
||||
import { ScoringModule } from './modules/scoring/scoring.module';
|
||||
import { AdminModule } from './modules/admin/admin.module';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { AppThrottlerGuard } from './common/guards/app-throttler.guard';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
|
||||
const ENV = process.env.NODE_ENV;
|
||||
|
||||
@@ -71,12 +71,13 @@ const ENV = process.env.NODE_ENV;
|
||||
ScheduleModule.forRoot(),
|
||||
|
||||
// Throttler
|
||||
ThrottlerModule.forRoot([
|
||||
{ name: 'default', limit: 100, ttl: 60_000 },
|
||||
{ name: 'login', limit: 5, ttl: 60_000 },
|
||||
{ name: 'otp', limit: 3, ttl: 300_000 },
|
||||
{ name: 'admin', limit: 30, ttl: 60_000 },
|
||||
]),
|
||||
// ThrottlerModule.forRoot([
|
||||
// { name: 'default', limit: 100, ttl: 60_000 },
|
||||
// { name: 'login', limit: 5, ttl: 60_000 },
|
||||
// { name: 'otp', limit: 3, ttl: 300_000 },
|
||||
// { name: 'admin', limit: 30, ttl: 60_000 },
|
||||
// ]),
|
||||
ThrottlerModule.forRoot([]),
|
||||
|
||||
/**
|
||||
* Local Modules
|
||||
|
||||
@@ -11,6 +11,7 @@ import { AdminQuestionsController } from './questions/admin-questions.controller
|
||||
import { AdminChoicesController } from './choices/admin-choices.controller';
|
||||
import { ScoringModule } from '../scoring/scoring.module';
|
||||
import { AdminScoringController } from './scoring/admin-scoring.controller';
|
||||
import { AdminQuestionTransitionsController } from './question-transitions/admin-question-transitions.controller';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -27,6 +28,7 @@ import { AdminScoringController } from './scoring/admin-scoring.controller';
|
||||
AdminQuestionsController,
|
||||
AdminChoicesController,
|
||||
AdminScoringController,
|
||||
AdminQuestionTransitionsController,
|
||||
],
|
||||
providers: [AdminAuthService],
|
||||
})
|
||||
|
||||
@@ -3,9 +3,9 @@ Authorization: Bearer {{$global.accessToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"question": 2,
|
||||
"text": "Option 3",
|
||||
"value": 3,
|
||||
"order": 3,
|
||||
"score": 6
|
||||
"question": 6,
|
||||
"text": "Option 2",
|
||||
"value": 1,
|
||||
"order": 2,
|
||||
"score": 8
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Role } from '@/common/enums/roles.enum';
|
||||
import { Roles } from '@/modules/auth/decorators/roles.decorator';
|
||||
import { CreateQuestionTransitionDTO } from '@/modules/questions/dtos/create-question-transition.dto';
|
||||
import { FindQuestionTransitionsDTO } from '@/modules/questions/dtos/find-question-transitions.dto';
|
||||
import { UpdateQuestionTransitionDTO } from '@/modules/questions/dtos/update-question-transition.dto';
|
||||
import { QuestionTransitionService } from '@/modules/questions/providers/transition/question-transition.service';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
|
||||
@Controller('admin/question-transitions')
|
||||
@Roles(Role.SUPERUSER, Role.ADMIN)
|
||||
@Throttle({ admin: {} })
|
||||
export class AdminQuestionTransitionsController {
|
||||
constructor(
|
||||
/**
|
||||
* Inject Question Transitions Service
|
||||
*/
|
||||
private readonly questionTransitionService: QuestionTransitionService,
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
public async createTransition(
|
||||
@Body() createQuestionTransitionDto: CreateQuestionTransitionDTO,
|
||||
) {
|
||||
return await this.questionTransitionService.createOne(
|
||||
createQuestionTransitionDto,
|
||||
);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
public async updateTransition(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateQuestionTransitionDto: UpdateQuestionTransitionDTO,
|
||||
) {
|
||||
return await this.questionTransitionService.updateOne(
|
||||
id,
|
||||
updateQuestionTransitionDto,
|
||||
);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
public async deleteTransition(@Param('id', ParseIntPipe) id: number) {
|
||||
return await this.questionTransitionService.deleteOne(id);
|
||||
}
|
||||
|
||||
@Get()
|
||||
public async getAllTransitions(@Query() query: FindQuestionTransitionsDTO) {
|
||||
return await this.questionTransitionService.findAllForAdmin(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
public async getTransitionById(@Param('id', ParseIntPipe) id: number) {
|
||||
return await this.questionTransitionService.findOneByIdForAdmin(id);
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
POST {{baseUrl}}/admin/question-transitions/
|
||||
Authorization: Bearer {{$global.accessToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"test": 1,
|
||||
"type": "end",
|
||||
"fromQuestion": 7
|
||||
// "toQuestion": 7
|
||||
// "conditionValue": {"min": 51, "max": 100}
|
||||
// "conditionValue": {"choiceId": 5}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ Content-Type: application/json
|
||||
|
||||
{
|
||||
"test": 1,
|
||||
"content": "سوال سوم",
|
||||
"content": "سوال هفتم",
|
||||
"required": true,
|
||||
"type": "numeric",
|
||||
"metadata": { "min": 1, "max": 20 }
|
||||
"type": "numeric"
|
||||
// "metadata": { "min": -10, "max": 10 }
|
||||
}
|
||||
|
||||
POST {{baseUrl}}/admin/questions/range-rules
|
||||
@@ -15,8 +15,8 @@ Authorization: Bearer {{$global.accessToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"question": 3,
|
||||
"question": 5,
|
||||
"min": 1,
|
||||
"max": 10,
|
||||
"score": 10
|
||||
"score": 8
|
||||
}
|
||||
@@ -3,7 +3,7 @@ Content-Type: application/json
|
||||
|
||||
{
|
||||
"phone": "09333026363",
|
||||
"otp": "87074"
|
||||
"otp": "55486"
|
||||
}
|
||||
|
||||
{{
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { PaginationQueryDTO } from '@/common/dtos/pagination-query.dto';
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNumber, IsOptional } from 'class-validator';
|
||||
import { QuestionTransitionType } from '../enums/question-transition-type.enum';
|
||||
import { QuestionTransitionSortField } from '../enums/question-transition-sort-field.enum';
|
||||
|
||||
export class FindQuestionTransitionsDTO extends PartialType(
|
||||
PaginationQueryDTO,
|
||||
) {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
id?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
test?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
fromQuestion?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
toQuestion?: number;
|
||||
|
||||
@IsEnum(QuestionTransitionType)
|
||||
@IsOptional()
|
||||
type?: QuestionTransitionType;
|
||||
|
||||
@IsEnum(QuestionTransitionSortField)
|
||||
@IsOptional()
|
||||
sortBy?: QuestionTransitionSortField;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export enum QuestionTransitionSortField {
|
||||
ID = 'id',
|
||||
TEST = 'test',
|
||||
FROM_QUESTION = 'fromQuestion',
|
||||
TO_QUESTION = 'toQuestion',
|
||||
TYPE = 'type',
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { FindOptionsWhere } from 'typeorm';
|
||||
import { FindQuestionTransitionsDTO } from '../dtos/find-question-transitions.dto';
|
||||
import { QuestionTransition } from '../entities/question-transition.entity';
|
||||
|
||||
export const filterQuestionTransitions = (
|
||||
query: FindQuestionTransitionsDTO,
|
||||
) => {
|
||||
const where: FindOptionsWhere<QuestionTransition> = {};
|
||||
|
||||
if (query.id) {
|
||||
where.id = query.id;
|
||||
}
|
||||
|
||||
if (query.type !== undefined) {
|
||||
where.type = query.type;
|
||||
}
|
||||
|
||||
return where;
|
||||
};
|
||||
@@ -61,9 +61,14 @@ export class QuestionRangeRuleCommandProvider {
|
||||
): Promise<void> {
|
||||
const queryBuilder = this.questionRangeRulesRepository
|
||||
.createQueryBuilder('rule')
|
||||
.where('rule.questionId = :question', { question })
|
||||
.where({ question: { id: 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)',
|
||||
`(
|
||||
(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 },
|
||||
);
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ export class QuestionTransitionCommandProvider {
|
||||
} catch (err) {
|
||||
this.handleDBExceptions(err);
|
||||
}
|
||||
|
||||
return newTransition;
|
||||
}
|
||||
|
||||
public async updateOne(
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { QuestionTransition } from '../../entities/question-transition.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { FindQuestionTransitionsDTO } from '../../dtos/find-question-transitions.dto';
|
||||
import { filterQuestionTransitions } from '../../helpers/filter-question-transitions.helper';
|
||||
import Utils from '@/common/utils';
|
||||
import { Test } from '@/modules/tests/entities/test.entity';
|
||||
import { TestsService } from '@/modules/tests/providers/tests.service';
|
||||
import { Question } from '../../entities/question.entity';
|
||||
import { QuestionsQueryProvider } from '../questions-query.provider';
|
||||
import { PaginatedResponse } from '@/common/responses';
|
||||
import { AppTimeoutException } from '@/common/exceptions/app-timeout.exception';
|
||||
import { AppException } from '@/common/exceptions/app.exception';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionTransitionQueryProvider {
|
||||
constructor(
|
||||
/**
|
||||
* Inject QuestionTransitions Repository
|
||||
*/
|
||||
@InjectRepository(QuestionTransition)
|
||||
private readonly transitionsRepository: Repository<QuestionTransition>,
|
||||
|
||||
/**
|
||||
* Inject Tests Service
|
||||
*/
|
||||
private readonly testsService: TestsService,
|
||||
|
||||
/**
|
||||
* Inject Questions Query Provider
|
||||
*/
|
||||
private readonly questionsQueryProvider: QuestionsQueryProvider,
|
||||
) {}
|
||||
|
||||
public async findAllForAdmin(query: FindQuestionTransitionsDTO) {
|
||||
const filters = filterQuestionTransitions(query);
|
||||
const { limit, page, skip, take } =
|
||||
Utils.PaginationUtils.getPaginationOptions(query);
|
||||
|
||||
let selectedTest: Test | null = null;
|
||||
let selectedFromQuestion: Question | null = null;
|
||||
let selectedToQuestion: Question | null = null;
|
||||
|
||||
if (query.test !== undefined) {
|
||||
selectedTest = await this.testsService.adminGetOne(query.test);
|
||||
}
|
||||
|
||||
if (query.fromQuestion !== undefined) {
|
||||
selectedFromQuestion = await this.questionsQueryProvider.findOneForAdmin({
|
||||
where: { id: query.fromQuestion },
|
||||
});
|
||||
}
|
||||
|
||||
if (query.toQuestion !== undefined) {
|
||||
selectedToQuestion = await this.questionsQueryProvider.findOneForAdmin({
|
||||
where: { id: query.toQuestion },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const [transitions, total] =
|
||||
await this.transitionsRepository.findAndCount({
|
||||
where: {
|
||||
...filters,
|
||||
...(selectedTest && {
|
||||
test: {
|
||||
id: selectedTest.id,
|
||||
},
|
||||
}),
|
||||
...(selectedFromQuestion && {
|
||||
fromQuestion: {
|
||||
id: selectedFromQuestion.id,
|
||||
},
|
||||
}),
|
||||
...(selectedToQuestion && {
|
||||
toQuestion: {
|
||||
id: selectedToQuestion.id,
|
||||
},
|
||||
}),
|
||||
},
|
||||
loadRelationIds: true,
|
||||
skip,
|
||||
take,
|
||||
order: query.sortBy
|
||||
? {
|
||||
[query.sortBy]: query.order ?? 'ASC',
|
||||
}
|
||||
: { id: 'DESC' },
|
||||
});
|
||||
|
||||
return new PaginatedResponse(transitions, {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new AppTimeoutException(err);
|
||||
}
|
||||
}
|
||||
|
||||
public async findOneByIdForAdmin(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)' },
|
||||
);
|
||||
}
|
||||
|
||||
return transition;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { QuestionTransitionCommandProvider } from './question-transition-command.provider';
|
||||
import { CreateQuestionTransitionDTO } from '../../dtos/create-question-transition.dto';
|
||||
import { UpdateQuestionTransitionDTO } from '../../dtos/update-question-transition.dto';
|
||||
import { FindQuestionTransitionsDTO } from '../../dtos/find-question-transitions.dto';
|
||||
import { QuestionTransitionQueryProvider } from './question-transition-query.provider';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionTransitionService {
|
||||
constructor(
|
||||
/**
|
||||
* Inject QuestionTransition Command Provider
|
||||
*/
|
||||
private readonly questionTransitionCommandProvider: QuestionTransitionCommandProvider,
|
||||
|
||||
/**
|
||||
* Inject QuestionTransition Query Provider
|
||||
*/
|
||||
private readonly questionTransitionQueryProvider: QuestionTransitionQueryProvider,
|
||||
) {}
|
||||
|
||||
public async createOne(
|
||||
createQuestionTransitionDto: CreateQuestionTransitionDTO,
|
||||
) {
|
||||
return await this.questionTransitionCommandProvider.createOne(
|
||||
createQuestionTransitionDto,
|
||||
);
|
||||
}
|
||||
|
||||
public async updateOne(
|
||||
id: number,
|
||||
updateQuestionTransitionDto: UpdateQuestionTransitionDTO,
|
||||
) {
|
||||
return await this.questionTransitionCommandProvider.updateOne(
|
||||
id,
|
||||
updateQuestionTransitionDto,
|
||||
);
|
||||
}
|
||||
|
||||
public async deleteOne(id: number) {
|
||||
return await this.questionTransitionCommandProvider.deleteOne(id);
|
||||
}
|
||||
|
||||
public async findAllForAdmin(query: FindQuestionTransitionsDTO) {
|
||||
return await this.questionTransitionQueryProvider.findAllForAdmin(query);
|
||||
}
|
||||
|
||||
public async findOneByIdForAdmin(id: number) {
|
||||
return await this.questionTransitionQueryProvider.findOneByIdForAdmin(id);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -140,7 +140,7 @@ export class CheckTransitionDBProvider {
|
||||
});
|
||||
|
||||
if (options.type === QuestionTransitionType.CHOICE) {
|
||||
qb.andWhere(`t.conditionValue ->> 'choiceId' = :choiceId`, {
|
||||
qb.andWhere(`t.condition ->> 'choiceId' = :choiceId`, {
|
||||
choiceId: String(
|
||||
(options.conditionValue as SingleChoiceCondition).choiceId,
|
||||
),
|
||||
@@ -148,9 +148,9 @@ export class CheckTransitionDBProvider {
|
||||
}
|
||||
|
||||
if (options.type === QuestionTransitionType.NUMBER_RANGE) {
|
||||
qb.andWhere(`t.conditionValue ->> 'min' = :min`, {
|
||||
qb.andWhere(`t.condition ->> 'min' = :min`, {
|
||||
min: String((options.conditionValue as NumericCondition).min),
|
||||
}).andWhere(`t.conditionValue ->> 'max' = :max`, {
|
||||
}).andWhere(`t.condition ->> 'max' = :max`, {
|
||||
max: String((options.conditionValue as NumericCondition).max),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@ import { QuestionRangeRuleQueryProvider } from './providers/range-rule/question-
|
||||
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';
|
||||
import { QuestionTransitionCommandProvider } from './providers/transition/question-transition-command.provider';
|
||||
import { QuestionTransitionQueryProvider } from './providers/transition/question-transition-query.provider';
|
||||
import { QuestionTransitionService } from './providers/transition/question-transition.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -48,7 +51,15 @@ import { QuestionTransitionValidatorProvider } from './providers/transition/vali
|
||||
// Question Transitions
|
||||
CheckTransitionDBProvider,
|
||||
QuestionTransitionValidatorProvider,
|
||||
QuestionTransitionCommandProvider,
|
||||
QuestionTransitionQueryProvider,
|
||||
QuestionTransitionService,
|
||||
],
|
||||
exports: [
|
||||
QuestionsService,
|
||||
ChoicesService,
|
||||
QuestionRangeRuleService,
|
||||
QuestionTransitionService,
|
||||
],
|
||||
exports: [QuestionsService, ChoicesService, QuestionRangeRuleService],
|
||||
})
|
||||
export class QuestionsModule {}
|
||||
|
||||
Reference in New Issue
Block a user