feat: add admin tests read and update endpoints

This commit is contained in:
2026-06-06 13:33:56 +03:30
parent 39e6c96113
commit 28c1a4831e
31 changed files with 330 additions and 101 deletions
+3 -1
View File
@@ -7,7 +7,9 @@
"isActive": "Is Active" "isActive": "Is Active"
}, },
"errors": { "errors": {
"intPrice": "Price should be an integer." "intPrice": "Price should be an integer.",
"testNotFound": "Test not found.",
"priceAccessConstraint": "Test's price depends on access type of 'Paid'."
}, },
"messages": { "messages": {
"createSuccess": "Test Created Successfully." "createSuccess": "Test Created Successfully."
+3 -1
View File
@@ -7,7 +7,9 @@
"isActive": "فعال بودن" "isActive": "فعال بودن"
}, },
"errors": { "errors": {
"intPrice": "قیمت باید عدد صحیح باشد." "intPrice": "قیمت باید عدد صحیح باشد.",
"testNotFound": "تست مورد نظر یافت نشد.",
"priceAccessConstraint": "قیمت تست به نوع دسترسی 'پرداخت' وابسته است."
}, },
"messages": { "messages": {
"createSuccess": "تست با موفقیت ایجاد شد." "createSuccess": "تست با موفقیت ایجاد شد."
@@ -19,5 +19,5 @@ export class AdminLoginDTO extends PhoneDTO {
field: '$t(auth.fields.password)', field: '$t(auth.fields.password)',
}), }),
}) })
password: string; password!: string;
} }
@@ -2,6 +2,6 @@ POST http://localhost:3000/admin/auth/login
Content-Type: application/json Content-Type: application/json
{ {
"phone": "0933302636", "phone": "09333026363",
"password": "Password1@" "password": "Password1@"
} }
@@ -1,10 +1,20 @@
import { Role } from '@/common/enums/roles.enum'; import { Role } from '@/common/enums/roles.enum';
import { Roles } from '@/modules/auth/decorators/roles.decorator'; import { Roles } from '@/modules/auth/decorators/roles.decorator';
import { CreateTestDTO } from '@/modules/tests/dtos/create-test.dto'; 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 { 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') @Controller('admin/tests')
@Roles(Role.SUPERUSER, Role.ADMIN)
export class AdminTestsController { export class AdminTestsController {
constructor( constructor(
/** /**
@@ -14,8 +24,25 @@ export class AdminTestsController {
) {} ) {}
@Post() @Post()
@Roles(Role.SUPERUSER, Role.ADMIN)
public async createTest(@Body() createTestDto: CreateTestDTO) { public async createTest(@Body() createTestDto: CreateTestDTO) {
return await this.testsService.createOne(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
}
+1 -1
View File
@@ -23,7 +23,7 @@ export class PhoneDTO {
}), }),
}) })
@Transform(({ value }) => Utils.StringUtils.normalizePhone(value as string)) @Transform(({ value }) => Utils.StringUtils.normalizePhone(value as string))
phone: string; phone!: string;
} }
export class LoginDTO extends PhoneDTO { export class LoginDTO extends PhoneDTO {
+1 -1
View File
@@ -3,5 +3,5 @@ import { IsNotEmpty, IsString } from 'class-validator';
export class RefreshTokenDTO { export class RefreshTokenDTO {
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
refreshToken: string; refreshToken!: string;
} }
+1 -1
View File
@@ -9,5 +9,5 @@ export class VerifyOtpDTO extends PhoneDTO {
field: '$t(auth.fields.otp)', field: '$t(auth.fields.otp)',
}), }),
}) })
otp: string; otp!: string;
} }
@@ -3,5 +3,6 @@ Content-Type: application/json
lang: fa lang: fa
{ {
"phone": "09121111111" "phone": "09333026363",
"password": "Password1@"
} }
@@ -2,10 +2,10 @@ POST http://localhost:3000/auth/refresh
Content-Type: application/json Content-Type: application/json
// { // {
// "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzgwMjIzNDExLCJleHAiOjE3ODAyMjcwMTEsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.FNTIrk6FKO8P88yKnRCybZ6E5N3KlJaCHqZt5DMVgds", // "access": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJwaG9uZSI6IjA5MzMzMDI2MzYzIiwiaWF0IjoxNzgwNDk0NDQ1LCJleHAiOjE3ODA0OTgwNDUsImF1ZCI6ImxvY2FsaG9zdCIsImlzcyI6ImxvY2FsaG9zdCJ9.Sy-Er8DXzEUCD6Z-GZav0XKmpIqf0sXeBkUzjIf4Ypw",
// "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJqdGkiOiJmZDRjZGViYS1iMjUzLTQ5MjgtOGIxYS0yZTJhZDMwOTIzYzMiLCJpYXQiOjE3ODAyMjM0MTEsImV4cCI6MTc4MTA4NzQxMSwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.EO0OpjQix794Y7yVZ8pjLH5HngYfCTwu3lonEfXtLIQ" // "refresh": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJqdGkiOiIxOTA0NDljZi01ZGFiLTQ4NDYtOGU0OS1jZmNkYjhlNjI1YmQiLCJpYXQiOjE3ODA0OTQ0NDUsImV4cCI6MTc4MTM1ODQ0NSwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.k3ATuOrenBFRVAGUy93Kv6iCkKF7vLB2DSohoH0JpXQ"
// } // }
{ {
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJqdGkiOiIyZjQ1YzcxMi01ZGQzLTRhY2ItYjExYS04ZDA5YTc3YTVmYTciLCJpYXQiOjE3Nzk5NTY1OTksImV4cCI6MTc4MDgyMDU5OSwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.GX4kSCUCIvBVq9l4lQ8dkETvFZ1NwBIm9NGOIjBsQCc" "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJqdGkiOiJmMDA0YWNmZS03ZWU5LTQ1OTgtYjIwZi04NGM3YTA5NGU4NGIiLCJpYXQiOjE3ODAzODkxNjksImV4cCI6MTc4MTI1MzE2OSwiYXVkIjoibG9jYWxob3N0IiwiaXNzIjoibG9jYWxob3N0In0.yGKx-j0ommIdr1ARYZrXQyXRA-ZIi5Am7QS-VWlkc8w"
} }
@@ -40,7 +40,6 @@ export class RefreshTokensProvider {
) {} ) {}
public async refreshTokens(refreshTokenDto: RefreshTokenDTO) { public async refreshTokens(refreshTokenDto: RefreshTokenDTO) {
try {
const payload = await this.jwtService.verifyAsync<RefreshTokenPayload>( const payload = await this.jwtService.verifyAsync<RefreshTokenPayload>(
refreshTokenDto.refreshToken, refreshTokenDto.refreshToken,
{ {
@@ -68,10 +67,5 @@ export class RefreshTokensProvider {
const newJti = randomUUID(); const newJti = randomUUID();
return await this.generateTokenProvider.generateTokens(user, newJti); return await this.generateTokenProvider.generateTokens(user, newJti);
} catch (err) {
throw new UnauthorizedException('auth.errors.unauthorized', {
cause: err,
});
}
} }
} }
+6 -6
View File
@@ -11,7 +11,7 @@ import {
@Index(['phone', 'used']) @Index(['phone', 'used'])
export class OTP { export class OTP {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id!: number;
@Index() @Index()
@Column({ @Column({
@@ -19,23 +19,23 @@ export class OTP {
length: 11, length: 11,
nullable: false, nullable: false,
}) })
phone: string; phone!: string;
@Column({ @Column({
type: 'varchar', type: 'varchar',
nullable: false, nullable: false,
}) })
code: string; code!: string;
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt!: Date;
@Index() @Index()
@Column({ type: 'timestamptz' }) @Column({ type: 'timestamptz' })
expiresAt: Date; expiresAt!: Date;
@Column({ type: 'boolean', default: false }) @Column({ type: 'boolean', default: false })
used: boolean; used!: boolean;
@BeforeInsert() @BeforeInsert()
setExpiry() { setExpiry() {
@@ -4,23 +4,23 @@ import { Question } from './question.entity';
@Entity() @Entity()
export class Choice { export class Choice {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id!: number;
@ManyToOne(() => Question, (question) => question.choices, { @ManyToOne(() => Question, (question) => question.choices, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
}) })
question: Question; question!: Question;
@Column({ @Column({
type: 'varchar', type: 'varchar',
length: 255, length: 255,
nullable: false, nullable: false,
}) })
text: string; text!: string;
@Column({ type: 'int', nullable: false }) @Column({ type: 'int', nullable: false })
value: number; value!: number;
@Column({ type: 'int', nullable: false }) @Column({ type: 'int', nullable: false })
order: number; order!: number;
} }
@@ -14,35 +14,35 @@ import { Choice } from './choice.entity';
@Entity() @Entity()
export class Question { export class Question {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id!: number;
@ManyToOne(() => Test, (test) => test.questions, { @ManyToOne(() => Test, (test) => test.questions, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
}) })
test: Test; test!: Test;
@Column({ @Column({
type: 'enum', type: 'enum',
enum: QuestionType, enum: QuestionType,
default: QuestionType.MULTI_CHOICE, default: QuestionType.MULTI_CHOICE,
}) })
type: QuestionType; type!: QuestionType;
@Column({ type: 'text' }) @Column({ type: 'text' })
content: string; content!: string;
@Column({ type: 'int', nullable: false }) @Column({ type: 'int', nullable: false })
order: number; order!: number;
@Column({ type: 'boolean', default: true }) @Column({ type: 'boolean', default: true })
required: boolean; required!: boolean;
@Column({ type: 'jsonb', nullable: true }) @Column({ type: 'jsonb', nullable: true })
metadata: QuestionMetadata; metadata!: QuestionMetadata;
@OneToMany(() => Choice, (choice) => choice.question) @OneToMany(() => Choice, (choice) => choice.question)
choices?: Array<Choice>; choices?: Array<Choice>;
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt!: Date;
} }
@@ -10,11 +10,11 @@ import {
@Entity() @Entity()
export class RuleSet { export class RuleSet {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id!: number;
@OneToOne(() => Test, (test) => test.ruleSet) @OneToOne(() => Test, (test) => test.ruleSet)
@JoinColumn() @JoinColumn()
test: Test; test!: Test;
@Column({ type: 'jsonb', nullable: false }) @Column({ type: 'jsonb', nullable: false })
config: any; config: any;
@@ -6,15 +6,15 @@ import { Choice } from '@/modules/questions/entities/choice.entity';
@Entity() @Entity()
export class SubmissionAnswer { export class SubmissionAnswer {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id!: number;
@ManyToOne(() => Submission, (submission) => submission.answers, { @ManyToOne(() => Submission, (submission) => submission.answers, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
}) })
submission: Submission; submission!: Submission;
@ManyToOne(() => Question, { onDelete: 'CASCADE' }) @ManyToOne(() => Question, { onDelete: 'CASCADE' })
question: Question; question!: Question;
@ManyToOne(() => Choice, { nullable: true, onDelete: 'CASCADE' }) @ManyToOne(() => Choice, { nullable: true, onDelete: 'CASCADE' })
choice?: Choice; choice?: Choice;
@@ -13,20 +13,20 @@ import { ResultRange } from '@/modules/scoring/entities/result-range.entity';
@Check(`"probability" >= 0 AND "probability" <= 100`) @Check(`"probability" >= 0 AND "probability" <= 100`)
export class SubmissionResult { export class SubmissionResult {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id!: number;
@ManyToOne(() => Submission, { onDelete: 'CASCADE' }) @ManyToOne(() => Submission, { onDelete: 'CASCADE' })
submission: Submission; submission!: Submission;
@Column({ type: 'int', nullable: false }) @Column({ type: 'int', nullable: false })
score: number; score!: number;
@Column({ type: 'int', nullable: false }) @Column({ type: 'int', nullable: false })
probability: number; probability!: number;
@ManyToOne(() => ResultRange, { onDelete: 'CASCADE' }) @ManyToOne(() => ResultRange, { onDelete: 'CASCADE' })
resultRange: ResultRange; resultRange!: ResultRange;
@CreateDateColumn() @CreateDateColumn()
createdAt: Date; createdAt!: Date;
} }
@@ -14,24 +14,24 @@ import { SubmissionAnswer } from './submission-answer.entity';
@Entity() @Entity()
export class Submission { export class Submission {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id!: number;
@ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' }) @ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' })
user?: User; user?: User;
@ManyToOne(() => Test, { onDelete: 'CASCADE' }) @ManyToOne(() => Test, { onDelete: 'CASCADE' })
test: Test; test!: Test;
@Column({ @Column({
type: 'enum', type: 'enum',
enum: SubmissionStatus, enum: SubmissionStatus,
default: SubmissionStatus.IN_PROGRESS, default: SubmissionStatus.IN_PROGRESS,
}) })
status: SubmissionStatus; status!: SubmissionStatus;
@OneToMany(() => SubmissionAnswer, (answer) => answer.submission) @OneToMany(() => SubmissionAnswer, (answer) => answer.submission)
answers: Array<SubmissionAnswer>; answers!: Array<SubmissionAnswer>;
@CreateDateColumn() @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);
};
+6 -2
View File
@@ -9,6 +9,7 @@ import {
} from 'class-validator'; } from 'class-validator';
import { i18nValidationMessage as t } from 'nestjs-i18n'; import { i18nValidationMessage as t } from 'nestjs-i18n';
import { AccessType } from '../enums/access-types.enum'; import { AccessType } from '../enums/access-types.enum';
import { PriceAccessType } from '../decorators/price-access-type.decorator';
export class CreateTestDTO { export class CreateTestDTO {
@IsString() @IsString()
@@ -23,7 +24,7 @@ export class CreateTestDTO {
max: 255, max: 255,
}), }),
}) })
title: string; title!: string;
@IsString() @IsString()
@IsNotEmpty({ @IsNotEmpty({
@@ -31,7 +32,7 @@ export class CreateTestDTO {
field: '$t(tests.fields.description)', field: '$t(tests.fields.description)',
}), }),
}) })
description: string; description!: string;
@IsEnum(AccessType) @IsEnum(AccessType)
@IsOptional() @IsOptional()
@@ -45,4 +46,7 @@ export class CreateTestDTO {
@IsBoolean() @IsBoolean()
@IsOptional() @IsOptional()
isActive?: boolean; 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) {}
+16 -10
View File
@@ -1,4 +1,5 @@
import { import {
Check,
Column, Column,
CreateDateColumn, CreateDateColumn,
Entity, Entity,
@@ -12,9 +13,14 @@ import { ResultRange } from '@/modules/scoring/entities/result-range.entity';
import { RuleSet } from '@/modules/scoring/entities/rule-set.entity'; import { RuleSet } from '@/modules/scoring/entities/rule-set.entity';
@Entity() @Entity()
@Check(`
("price" IS NULL AND "accessType" <> 'paid')
OR
("price" IS NOT NULL AND "accessType" = 'paid')
`)
export class Test { export class Test {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
id: number; id!: number;
@Column({ @Column({
type: 'varchar', type: 'varchar',
@@ -22,42 +28,42 @@ export class Test {
nullable: false, nullable: false,
unique: true, unique: true,
}) })
title: string; title!: string;
@Column({ @Column({
type: 'text', type: 'text',
nullable: false, nullable: false,
}) })
description: string; description!: string;
@Column({ @Column({
type: 'enum', type: 'enum',
enum: AccessType, enum: AccessType,
default: AccessType.LOGIN, default: AccessType.LOGIN,
}) })
accessType: AccessType; accessType!: AccessType;
@Column({ @Column({
type: 'bigint', type: 'bigint',
nullable: true, nullable: true,
}) })
price: string; price!: string;
@Column({ @Column({
type: 'boolean', type: 'boolean',
default: false, default: false,
}) })
isActive: boolean; isActive!: boolean;
@OneToMany(() => Question, (question) => question.test) @OneToMany(() => Question, (question) => question.test)
questions: Array<Question>; questions!: Array<Question>;
@OneToOne(() => RuleSet, (ruleSet) => ruleSet.test) @OneToOne(() => RuleSet, (ruleSet) => ruleSet.test)
ruleSet: RuleSet; ruleSet!: RuleSet;
@OneToMany(() => ResultRange, (range) => range.test) @OneToMany(() => ResultRange, (range) => range.test)
resultRanges: Array<ResultRange>; resultRanges!: Array<ResultRange>;
@CreateDateColumn() @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 { import {
BadRequestException,
HttpStatus, HttpStatus,
Injectable, Injectable,
NotFoundException,
RequestTimeoutException, RequestTimeoutException,
} from '@nestjs/common'; } from '@nestjs/common';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@@ -10,6 +12,8 @@ import { CreateTestDTO } from '../dtos/create-test.dto';
import { AppException } from '@/common/exceptions/app.exception'; import { AppException } from '@/common/exceptions/app.exception';
import { AppResponse } from '@/common/responses'; import { AppResponse } from '@/common/responses';
import { I18nService } from 'nestjs-i18n'; import { I18nService } from 'nestjs-i18n';
import { UpdateTestDTO } from '../dtos/update-test.dto';
import { AccessType } from '../enums/access-types.enum';
@Injectable() @Injectable()
export class TestsService { 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',
});
}
}
} }
+25
View File
@@ -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
View File
@@ -2,9 +2,11 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; import { TypeOrmModule } from '@nestjs/typeorm';
import { Test } from './entities/test.entity'; import { Test } from './entities/test.entity';
import { TestsService } from './providers/tests.service'; import { TestsService } from './providers/tests.service';
import { TestsController } from './tests.controller';
@Module({ @Module({
imports: [TypeOrmModule.forFeature([Test])], imports: [TypeOrmModule.forFeature([Test])],
controllers: [TestsController],
providers: [TestsService], providers: [TestsService],
exports: [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'."
);
}
}