feat: add admin tests read and update endpoints
This commit is contained in:
@@ -19,5 +19,5 @@ export class AdminLoginDTO extends PhoneDTO {
|
||||
field: '$t(auth.fields.password)',
|
||||
}),
|
||||
})
|
||||
password: string;
|
||||
password!: string;
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,6 +2,6 @@ POST http://localhost:3000/admin/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"phone": "0933302636",
|
||||
"phone": "09333026363",
|
||||
"password": "Password1@"
|
||||
}
|
||||
@@ -1,10 +1,20 @@
|
||||
import { Role } from '@/common/enums/roles.enum';
|
||||
import { Roles } from '@/modules/auth/decorators/roles.decorator';
|
||||
import { CreateTestDTO } from '@/modules/tests/dtos/create-test.dto';
|
||||
import { UpdateTestDTO } from '@/modules/tests/dtos/update-test.dto';
|
||||
import { TestsService } from '@/modules/tests/providers/tests.service';
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Patch,
|
||||
Post,
|
||||
} from '@nestjs/common';
|
||||
|
||||
@Controller('admin/tests')
|
||||
@Roles(Role.SUPERUSER, Role.ADMIN)
|
||||
export class AdminTestsController {
|
||||
constructor(
|
||||
/**
|
||||
@@ -14,8 +24,25 @@ export class AdminTestsController {
|
||||
) {}
|
||||
|
||||
@Post()
|
||||
@Roles(Role.SUPERUSER, Role.ADMIN)
|
||||
public async createTest(@Body() createTestDto: CreateTestDTO) {
|
||||
return await this.testsService.createOne(createTestDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
public async getAllTests() {
|
||||
return await this.testsService.adminGetAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
public async getTestById(@Param('id', ParseIntPipe) id: number) {
|
||||
return await this.testsService.adminGetOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
public async updateTestById(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateTestDto: UpdateTestDTO,
|
||||
) {
|
||||
return await this.testsService.updateOne(id, updateTestDto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
GET http://localhost:3000/admin/tests
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzgwMzk4MzI0LCJleHAiOjE3ODA0MDE5MjQsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.s3NAErSAjMTz2s27obas3TEi5Og2HPeLgWCoIyzmzII
|
||||
|
||||
GET http://localhost:3000/admin/tests/4
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzgwMzk4MzI0LCJleHAiOjE3ODA0MDE5MjQsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.s3NAErSAjMTz2s27obas3TEi5Og2HPeLgWCoIyzmzII
|
||||
@@ -0,0 +1,8 @@
|
||||
PATCH http://localhost:3000/admin/tests/4
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzgwNDk0NDQ1LCJleHAiOjE3ODA0OTgwNDUsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.Sy-Er8DXzEUCD6Z-GZav0XKmpIqf0sXeBkUzjIf4Ypw
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"accessType": "login",
|
||||
"isActive": false
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
POST http://localhost:3000/admin/tests/
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzgwNDk0NDQ1LCJleHAiOjE3ODA0OTgwNDUsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.Sy-Er8DXzEUCD6Z-GZav0XKmpIqf0sXeBkUzjIf4Ypw
|
||||
|
||||
{
|
||||
"title": "Test Five",
|
||||
"description": "Description Five",
|
||||
"accessType": "login",
|
||||
"price": "1234000",
|
||||
"isActive": true
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
POST http://localhost:3000/admin/tests/
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzgwMjIzNDExLCJleHAiOjE3ODAyMjcwMTEsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.FNTIrk6FKO8P88yKnRCybZ6E5N3KlJaCHqZt5DMVgds
|
||||
|
||||
{
|
||||
"title": "Test Three",
|
||||
"description": "Description Three",
|
||||
"accessType": "free",
|
||||
"price": "1234000",
|
||||
"isActive": true
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export class PhoneDTO {
|
||||
}),
|
||||
})
|
||||
@Transform(({ value }) => Utils.StringUtils.normalizePhone(value as string))
|
||||
phone: string;
|
||||
phone!: string;
|
||||
}
|
||||
|
||||
export class LoginDTO extends PhoneDTO {
|
||||
|
||||
@@ -3,5 +3,5 @@ import { IsNotEmpty, IsString } from 'class-validator';
|
||||
export class RefreshTokenDTO {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
refreshToken: string;
|
||||
refreshToken!: string;
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ export class VerifyOtpDTO extends PhoneDTO {
|
||||
field: '$t(auth.fields.otp)',
|
||||
}),
|
||||
})
|
||||
otp: string;
|
||||
otp!: string;
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ Content-Type: application/json
|
||||
lang: fa
|
||||
|
||||
{
|
||||
"phone": "09121111111"
|
||||
"phone": "09333026363",
|
||||
"password": "Password1@"
|
||||
}
|
||||
@@ -2,10 +2,10 @@ POST http://localhost:3000/auth/refresh
|
||||
Content-Type: application/json
|
||||
|
||||
// {
|
||||
// "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzgwMjIzNDExLCJleHAiOjE3ODAyMjcwMTEsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.FNTIrk6FKO8P88yKnRCybZ6E5N3KlJaCHqZt5DMVgds",
|
||||
// "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJqdGkiOiJmZDRjZGViYS1iMjUzLTQ5MjgtOGIxYS0yZTJhZDMwOTIzYzMiLCJpYXQiOjE3ODAyMjM0MTEsImV4cCI6MTc4MTA4NzQxMSwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.EO0OpjQix794Y7yVZ8pjLH5HngYfCTwu3lonEfXtLIQ"
|
||||
// "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzgwNDk0NDQ1LCJleHAiOjE3ODA0OTgwNDUsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.Sy-Er8DXzEUCD6Z-GZav0XKmpIqf0sXeBkUzjIf4Ypw",
|
||||
// "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJqdGkiOiIxOTA0NDljZi01ZGFiLTQ4NDYtOGU0OS1jZmNkYjhlNjI1YmQiLCJpYXQiOjE3ODA0OTQ0NDUsImV4cCI6MTc4MTM1ODQ0NSwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.k3ATuOrenBFRVAGUy93Kv6iCkKF7vLB2DSohoH0JpXQ"
|
||||
// }
|
||||
|
||||
{
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJqdGkiOiIyZjQ1YzcxMi01ZGQzLTRhY2ItYjExYS04ZDA5YTc3YTVmYTciLCJpYXQiOjE3Nzk5NTY1OTksImV4cCI6MTc4MDgyMDU5OSwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.GX4kSCUCIvBVq9l4lQ8dkETvFZ1NwBIm9NGOIjBsQCc"
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJqdGkiOiJmMDA0YWNmZS03ZWU5LTQ1OTgtYjIwZi04NGM3YTA5NGU4NGIiLCJpYXQiOjE3ODAzODkxNjksImV4cCI6MTc4MTI1MzE2OSwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.yGKx-j0ommIdr1ARYZrXQyXRA-ZIi5Am7QS-VWlkc8w"
|
||||
}
|
||||
|
||||
@@ -40,38 +40,32 @@ export class RefreshTokensProvider {
|
||||
) {}
|
||||
|
||||
public async refreshTokens(refreshTokenDto: RefreshTokenDTO) {
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync<RefreshTokenPayload>(
|
||||
refreshTokenDto.refreshToken,
|
||||
{
|
||||
secret: this.jwtConfiguration.refresh.secret,
|
||||
issuer: this.jwtConfiguration.issuer,
|
||||
audience: this.jwtConfiguration.audience,
|
||||
},
|
||||
);
|
||||
const payload = await this.jwtService.verifyAsync<RefreshTokenPayload>(
|
||||
refreshTokenDto.refreshToken,
|
||||
{
|
||||
secret: this.jwtConfiguration.refresh.secret,
|
||||
issuer: this.jwtConfiguration.issuer,
|
||||
audience: this.jwtConfiguration.audience,
|
||||
},
|
||||
);
|
||||
|
||||
const user = await this.usersService.findOneById(payload.sub);
|
||||
const user = await this.usersService.findOneById(payload.sub);
|
||||
|
||||
if (!payload.jti) {
|
||||
throw new UnauthorizedException('auth.errors.unauthorized');
|
||||
}
|
||||
|
||||
const existsInRedis = await this.authRedisProvider.hasRefreshToken(
|
||||
payload.jti,
|
||||
);
|
||||
|
||||
if (!existsInRedis) {
|
||||
throw new UnauthorizedException('auth.errors.tokenReuse');
|
||||
}
|
||||
|
||||
await this.authRedisProvider.revokeRefreshToken(payload.jti);
|
||||
|
||||
const newJti = randomUUID();
|
||||
return await this.generateTokenProvider.generateTokens(user, newJti);
|
||||
} catch (err) {
|
||||
throw new UnauthorizedException('auth.errors.unauthorized', {
|
||||
cause: err,
|
||||
});
|
||||
if (!payload.jti) {
|
||||
throw new UnauthorizedException('auth.errors.unauthorized');
|
||||
}
|
||||
|
||||
const existsInRedis = await this.authRedisProvider.hasRefreshToken(
|
||||
payload.jti,
|
||||
);
|
||||
|
||||
if (!existsInRedis) {
|
||||
throw new UnauthorizedException('auth.errors.tokenReuse');
|
||||
}
|
||||
|
||||
await this.authRedisProvider.revokeRefreshToken(payload.jti);
|
||||
|
||||
const newJti = randomUUID();
|
||||
return await this.generateTokenProvider.generateTokens(user, newJti);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
@Index(['phone', 'used'])
|
||||
export class OTP {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
id!: number;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
@@ -19,23 +19,23 @@ export class OTP {
|
||||
length: 11,
|
||||
nullable: false,
|
||||
})
|
||||
phone: string;
|
||||
phone!: string;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
nullable: false,
|
||||
})
|
||||
code: string;
|
||||
code!: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
createdAt!: Date;
|
||||
|
||||
@Index()
|
||||
@Column({ type: 'timestamptz' })
|
||||
expiresAt: Date;
|
||||
expiresAt!: Date;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
used: boolean;
|
||||
used!: boolean;
|
||||
|
||||
@BeforeInsert()
|
||||
setExpiry() {
|
||||
|
||||
@@ -4,23 +4,23 @@ import { Question } from './question.entity';
|
||||
@Entity()
|
||||
export class Choice {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
id!: number;
|
||||
|
||||
@ManyToOne(() => Question, (question) => question.choices, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
question: Question;
|
||||
question!: Question;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
length: 255,
|
||||
nullable: false,
|
||||
})
|
||||
text: string;
|
||||
text!: string;
|
||||
|
||||
@Column({ type: 'int', nullable: false })
|
||||
value: number;
|
||||
value!: number;
|
||||
|
||||
@Column({ type: 'int', nullable: false })
|
||||
order: number;
|
||||
order!: number;
|
||||
}
|
||||
|
||||
@@ -14,35 +14,35 @@ import { Choice } from './choice.entity';
|
||||
@Entity()
|
||||
export class Question {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
id!: number;
|
||||
|
||||
@ManyToOne(() => Test, (test) => test.questions, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
test: Test;
|
||||
test!: Test;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: QuestionType,
|
||||
default: QuestionType.MULTI_CHOICE,
|
||||
})
|
||||
type: QuestionType;
|
||||
type!: QuestionType;
|
||||
|
||||
@Column({ type: 'text' })
|
||||
content: string;
|
||||
content!: string;
|
||||
|
||||
@Column({ type: 'int', nullable: false })
|
||||
order: number;
|
||||
order!: number;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
required: boolean;
|
||||
required!: boolean;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
metadata: QuestionMetadata;
|
||||
metadata!: QuestionMetadata;
|
||||
|
||||
@OneToMany(() => Choice, (choice) => choice.question)
|
||||
choices?: Array<Choice>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
createdAt!: Date;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
@Entity()
|
||||
export class RuleSet {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
id!: number;
|
||||
|
||||
@OneToOne(() => Test, (test) => test.ruleSet)
|
||||
@JoinColumn()
|
||||
test: Test;
|
||||
test!: Test;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: false })
|
||||
config: any;
|
||||
|
||||
@@ -6,15 +6,15 @@ import { Choice } from '@/modules/questions/entities/choice.entity';
|
||||
@Entity()
|
||||
export class SubmissionAnswer {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
id!: number;
|
||||
|
||||
@ManyToOne(() => Submission, (submission) => submission.answers, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
submission: Submission;
|
||||
submission!: Submission;
|
||||
|
||||
@ManyToOne(() => Question, { onDelete: 'CASCADE' })
|
||||
question: Question;
|
||||
question!: Question;
|
||||
|
||||
@ManyToOne(() => Choice, { nullable: true, onDelete: 'CASCADE' })
|
||||
choice?: Choice;
|
||||
|
||||
@@ -13,20 +13,20 @@ import { ResultRange } from '@/modules/scoring/entities/result-range.entity';
|
||||
@Check(`"probability" >= 0 AND "probability" <= 100`)
|
||||
export class SubmissionResult {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
id!: number;
|
||||
|
||||
@ManyToOne(() => Submission, { onDelete: 'CASCADE' })
|
||||
submission: Submission;
|
||||
submission!: Submission;
|
||||
|
||||
@Column({ type: 'int', nullable: false })
|
||||
score: number;
|
||||
score!: number;
|
||||
|
||||
@Column({ type: 'int', nullable: false })
|
||||
probability: number;
|
||||
probability!: number;
|
||||
|
||||
@ManyToOne(() => ResultRange, { onDelete: 'CASCADE' })
|
||||
resultRange: ResultRange;
|
||||
resultRange!: ResultRange;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
createdAt!: Date;
|
||||
}
|
||||
|
||||
@@ -14,24 +14,24 @@ import { SubmissionAnswer } from './submission-answer.entity';
|
||||
@Entity()
|
||||
export class Submission {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
id!: number;
|
||||
|
||||
@ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' })
|
||||
user?: User;
|
||||
|
||||
@ManyToOne(() => Test, { onDelete: 'CASCADE' })
|
||||
test: Test;
|
||||
test!: Test;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: SubmissionStatus,
|
||||
default: SubmissionStatus.IN_PROGRESS,
|
||||
})
|
||||
status: SubmissionStatus;
|
||||
status!: SubmissionStatus;
|
||||
|
||||
@OneToMany(() => SubmissionAnswer, (answer) => answer.submission)
|
||||
answers: Array<SubmissionAnswer>;
|
||||
answers!: Array<SubmissionAnswer>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
createdAt!: Date;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Validate } from 'class-validator';
|
||||
import { PriceAccessTypeConstraint } from '../validators/price-access-type-constraint.validator';
|
||||
|
||||
export const PriceAccessType = () => {
|
||||
return Validate(PriceAccessTypeConstraint);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from 'class-validator';
|
||||
import { i18nValidationMessage as t } from 'nestjs-i18n';
|
||||
import { AccessType } from '../enums/access-types.enum';
|
||||
import { PriceAccessType } from '../decorators/price-access-type.decorator';
|
||||
|
||||
export class CreateTestDTO {
|
||||
@IsString()
|
||||
@@ -23,7 +24,7 @@ export class CreateTestDTO {
|
||||
max: 255,
|
||||
}),
|
||||
})
|
||||
title: string;
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty({
|
||||
@@ -31,7 +32,7 @@ export class CreateTestDTO {
|
||||
field: '$t(tests.fields.description)',
|
||||
}),
|
||||
})
|
||||
description: string;
|
||||
description!: string;
|
||||
|
||||
@IsEnum(AccessType)
|
||||
@IsOptional()
|
||||
@@ -45,4 +46,7 @@ export class CreateTestDTO {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isActive?: boolean;
|
||||
|
||||
@PriceAccessType()
|
||||
private readonly _priceAccessValidation!: never;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/swagger';
|
||||
import { CreateTestDTO } from './create-test.dto';
|
||||
|
||||
export class UpdateTestDTO extends PartialType(CreateTestDTO) {}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Check,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
@@ -12,9 +13,14 @@ import { ResultRange } from '@/modules/scoring/entities/result-range.entity';
|
||||
import { RuleSet } from '@/modules/scoring/entities/rule-set.entity';
|
||||
|
||||
@Entity()
|
||||
@Check(`
|
||||
("price" IS NULL AND "accessType" <> 'paid')
|
||||
OR
|
||||
("price" IS NOT NULL AND "accessType" = 'paid')
|
||||
`)
|
||||
export class Test {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
id!: number;
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
@@ -22,42 +28,42 @@ export class Test {
|
||||
nullable: false,
|
||||
unique: true,
|
||||
})
|
||||
title: string;
|
||||
title!: string;
|
||||
|
||||
@Column({
|
||||
type: 'text',
|
||||
nullable: false,
|
||||
})
|
||||
description: string;
|
||||
description!: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: AccessType,
|
||||
default: AccessType.LOGIN,
|
||||
})
|
||||
accessType: AccessType;
|
||||
accessType!: AccessType;
|
||||
|
||||
@Column({
|
||||
type: 'bigint',
|
||||
nullable: true,
|
||||
})
|
||||
price: string;
|
||||
price!: string;
|
||||
|
||||
@Column({
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
})
|
||||
isActive: boolean;
|
||||
isActive!: boolean;
|
||||
|
||||
@OneToMany(() => Question, (question) => question.test)
|
||||
questions: Array<Question>;
|
||||
questions!: Array<Question>;
|
||||
|
||||
@OneToOne(() => RuleSet, (ruleSet) => ruleSet.test)
|
||||
ruleSet: RuleSet;
|
||||
ruleSet!: RuleSet;
|
||||
|
||||
@OneToMany(() => ResultRange, (range) => range.test)
|
||||
resultRanges: Array<ResultRange>;
|
||||
resultRanges!: Array<ResultRange>;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
createdAt!: Date;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GET http://localhost:3000/tests
|
||||
|
||||
GET http://localhost:3000/tests/4
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
HttpStatus,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
RequestTimeoutException,
|
||||
} from '@nestjs/common';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -10,6 +12,8 @@ import { CreateTestDTO } from '../dtos/create-test.dto';
|
||||
import { AppException } from '@/common/exceptions/app.exception';
|
||||
import { AppResponse } from '@/common/responses';
|
||||
import { I18nService } from 'nestjs-i18n';
|
||||
import { UpdateTestDTO } from '../dtos/update-test.dto';
|
||||
import { AccessType } from '../enums/access-types.enum';
|
||||
|
||||
@Injectable()
|
||||
export class TestsService {
|
||||
@@ -54,4 +58,106 @@ export class TestsService {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async adminGetAll() {
|
||||
try {
|
||||
return await this.testRepository.find({ order: { createdAt: 'DESC' } });
|
||||
} catch (err) {
|
||||
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||
cause: err,
|
||||
description: 'Error connecting to the database',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async userGetAll() {
|
||||
try {
|
||||
return await this.testRepository.find({
|
||||
where: { isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
accessType: true,
|
||||
description: true,
|
||||
price: true,
|
||||
title: true,
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
} catch (err) {
|
||||
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||
cause: err,
|
||||
description: 'Error connecting to the database',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async adminGetOne(id: number) {
|
||||
try {
|
||||
return await this.testRepository.findOneBy({ id });
|
||||
} catch (err) {
|
||||
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||
cause: err,
|
||||
description: 'Error connecting to the database',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async userGetOne(id: number) {
|
||||
let test: Test | null;
|
||||
|
||||
try {
|
||||
test = await this.testRepository.findOne({
|
||||
where: { id, isActive: true },
|
||||
select: {
|
||||
id: true,
|
||||
accessType: true,
|
||||
description: true,
|
||||
price: true,
|
||||
title: true,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||
cause: err,
|
||||
description: 'Error connecting to the database',
|
||||
});
|
||||
}
|
||||
|
||||
if (!test) {
|
||||
throw new NotFoundException('tests.errors.testNotFound');
|
||||
}
|
||||
|
||||
return test;
|
||||
}
|
||||
|
||||
public async updateOne(id: number, updateTestDto: UpdateTestDTO) {
|
||||
const test = await this.testRepository.findOneBy({ id });
|
||||
|
||||
if (!test) {
|
||||
throw new NotFoundException('tests.errors.testNotFound');
|
||||
}
|
||||
|
||||
const accessType = updateTestDto.accessType ?? test.accessType;
|
||||
const price = updateTestDto.price ?? test.price;
|
||||
|
||||
if (
|
||||
(accessType === AccessType.PAID && !price) ||
|
||||
(accessType !== AccessType.PAID && price)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
this.i18nService.translate('tests.errors.priceAccessConstraint'),
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(test, updateTestDto);
|
||||
|
||||
try {
|
||||
return await this.testRepository.save(test);
|
||||
} catch (err) {
|
||||
throw new RequestTimeoutException('common.errors.requestTimeout', {
|
||||
cause: err,
|
||||
description: 'Error connecting to the database',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
|
||||
import { TestsService } from './providers/tests.service';
|
||||
import { Public } from '../auth/decorators/public.decorator';
|
||||
|
||||
@Controller('tests')
|
||||
export class TestsController {
|
||||
constructor(
|
||||
/**
|
||||
* Inject Tests Service
|
||||
*/
|
||||
private readonly testsService: TestsService,
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@Get()
|
||||
public async getAllTests() {
|
||||
return await this.testsService.userGetAll();
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get(':id')
|
||||
public async getTestById(@Param('id', ParseIntPipe) id: number) {
|
||||
return await this.testsService.userGetOne(id);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Test } from './entities/test.entity';
|
||||
import { TestsService } from './providers/tests.service';
|
||||
import { TestsController } from './tests.controller';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Test])],
|
||||
controllers: [TestsController],
|
||||
providers: [TestsService],
|
||||
exports: [TestsService],
|
||||
})
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
ValidationArguments,
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
import { AccessType } from '../enums/access-types.enum';
|
||||
import { I18nContext } from 'nestjs-i18n';
|
||||
|
||||
@ValidatorConstraint({ name: 'priceAccessType' })
|
||||
export class PriceAccessTypeConstraint implements ValidatorConstraintInterface {
|
||||
validate(
|
||||
_: any,
|
||||
validationArguments?: ValidationArguments,
|
||||
): Promise<boolean> | boolean {
|
||||
console.log('This is happening');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const obj = validationArguments?.object as any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { price, accessType } = obj;
|
||||
|
||||
return (
|
||||
(accessType === AccessType.PAID && price != null) ||
|
||||
(accessType !== AccessType.PAID && price == null)
|
||||
);
|
||||
}
|
||||
defaultMessage(): string {
|
||||
const i18n = I18nContext.current();
|
||||
return (
|
||||
i18n?.t('tests.errors.priceAccessConstraint') ??
|
||||
"Test's price depends to access type of 'Paid'."
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user