feat: add admin tests read and update endpoints
This commit is contained in:
@@ -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."
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import { IsNotEmpty, IsString } from 'class-validator';
|
|||||||
export class RefreshTokenDTO {
|
export class RefreshTokenDTO {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
refreshToken: string;
|
refreshToken!: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -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) {}
|
||||||
@@ -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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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'."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user