feat: add medical test entities

This commit is contained in:
2026-05-23 11:02:20 +03:30
parent 7186af8e7f
commit 7dda2564a8
32 changed files with 389 additions and 11 deletions
+8
View File
@@ -16,6 +16,10 @@ import redisConfig from './config/redis.config';
import { APP_FILTER } from '@nestjs/core';
import { GlobalExceptionFilter } from './common/filters/global-exception.filter';
import { ScheduleModule } from '@nestjs/schedule';
import { TestsModule } from './modules/tests/tests.module';
import { QuestionsModule } from './modules/questions/questions.module';
import { SubmissionsModule } from './modules/submissions/submissions.module';
import { ScoringModule } from './modules/scoring/scoring.module';
const ENV = process.env.NODE_ENV;
@@ -71,6 +75,10 @@ const ENV = process.env.NODE_ENV;
OtpModule,
HashingModule,
RedisModule,
TestsModule,
QuestionsModule,
SubmissionsModule,
ScoringModule,
],
providers: [
{
@@ -1,4 +1,4 @@
import { User } from '@/modules/users/user.entity';
import { User } from '@/modules/users/entities/user.entity';
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import type { Request } from 'express';
import { REQUEST_USER_KEY } from '../constants/request-headers.constants';
+5
View File
@@ -0,0 +1,5 @@
export enum Role {
SUPERUSER = 'superuser',
ADMIN = 'admin',
USER = 'user',
}
@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { Inject, Injectable } from '@nestjs/common';
import { REDIS } from '../constants/redis.constants';
@@ -1,5 +1,5 @@
import jwtConfig from '@/config/jwt.config';
import { User } from '@/modules/users/user.entity';
import { User } from '@/modules/users/entities/user.entity';
import { Inject, Injectable } from '@nestjs/common';
import type { ConfigType } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
+1 -1
View File
@@ -4,7 +4,7 @@ import type { ConfigType } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AccessTokenPayload } from '../interfaces/jwt.interface';
import { User } from '@/modules/users/user.entity';
import { User } from '@/modules/users/entities/user.entity';
import { UsersService } from '@/modules/users/providers/users.service';
@Injectable()
+1 -1
View File
@@ -1,7 +1,7 @@
import { Module } from '@nestjs/common';
import { OtpService } from './providers/otp.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OTP } from './otp.entity';
import { OTP } from './entities/otp.entity';
import { HashingModule } from '@/common/modules/hashing/hashing.module';
@Module({
+1 -1
View File
@@ -5,7 +5,7 @@ import {
UnauthorizedException,
} from '@nestjs/common';
import { LessThan, MoreThanOrEqual, Repository } from 'typeorm';
import { OTP } from '../otp.entity';
import { OTP } from '../entities/otp.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Cron } from '@nestjs/schedule';
import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider';
@@ -0,0 +1,26 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Question } from './question.entity';
@Entity()
export class Choice {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => Question, (question) => question.choices, {
onDelete: 'CASCADE',
})
question: Question;
@Column({
type: 'varchar',
length: 255,
nullable: false,
})
text: string;
@Column({ type: 'int', nullable: false })
value: number;
@Column({ type: 'int', nullable: false })
order: number;
}
@@ -0,0 +1,48 @@
import { Test } from '@/modules/tests/entities/test.entity';
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { QuestionType } from '../enums/question-type.enum';
import type { QuestionMetadata } from '../types/question-metadata.type';
import { Choice } from './choice.entity';
@Entity()
export class Question {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => Test, (test) => test.questions, {
onDelete: 'CASCADE',
})
test: Test;
@Column({
type: 'enum',
enum: QuestionType,
default: QuestionType.MULTI_CHOICE,
})
type: QuestionType;
@Column({ type: 'text' })
content: string;
@Column({ type: 'int', nullable: false })
order: number;
@Column({ type: 'boolean', default: true })
required: boolean;
@Column({ type: 'jsonb', nullable: true })
metadata: QuestionMetadata;
@OneToMany(() => Choice, (choice) => choice.question)
choices?: Array<Choice>;
@CreateDateColumn()
createdAt: Date;
}
@@ -0,0 +1,5 @@
export enum QuestionType {
MULTI_CHOICE = 'multi-choice',
NUMERIC = 'numeric',
TEXT = 'text',
}
@@ -0,0 +1,3 @@
export interface MultiChoiceMetadata {
allowOther?: boolean;
}
@@ -0,0 +1,5 @@
export interface NumericMetadata {
min: number;
max: number;
step?: number;
}
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Question } from './entities/question.entity';
import { Choice } from './entities/choice.entity';
@Module({
imports: [TypeOrmModule.forFeature([Question, Choice])],
})
export class QuestionsModule {}
@@ -0,0 +1,4 @@
import { MultiChoiceMetadata } from '../interfaces/multi-choice-metadata.interface';
import { NumericMetadata } from '../interfaces/numeric-metadata.interface';
export type QuestionMetadata = NumericMetadata | MultiChoiceMetadata;
@@ -0,0 +1,38 @@
import { Test } from '@/modules/tests/entities/test.entity';
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class ResultRange {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => Test, (test) => test.resultRanges, {
onDelete: 'CASCADE',
})
test: Test;
@Column({
type: 'int',
nullable: false,
})
minScore: number;
@Column({
type: 'int',
nullable: false,
})
maxScore: number;
@Column({
type: 'varchar',
length: 255,
nullable: false,
})
title: string;
@Column({
type: 'text',
nullable: false,
})
description: string;
}
@@ -0,0 +1,21 @@
import { Test } from '@/modules/tests/entities/test.entity';
import {
Column,
Entity,
JoinColumn,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
@Entity()
export class RuleSet {
@PrimaryGeneratedColumn()
id: number;
@OneToOne(() => Test, (test) => test.ruleSet)
@JoinColumn()
test: Test;
@Column({ type: 'jsonb', nullable: false })
config: any;
}
+9
View File
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ResultRange } from './entities/result-range.entity';
import { RuleSet } from './entities/rule-set.entity';
@Module({
imports: [TypeOrmModule.forFeature([ResultRange, RuleSet])],
})
export class ScoringModule {}
@@ -0,0 +1,31 @@
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Submission } from './submission.entity';
import { Question } from '@/modules/questions/entities/question.entity';
import { Choice } from '@/modules/questions/entities/choice.entity';
@Entity()
export class SubmissionAnswer {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => Submission, (submission) => submission.answers, {
onDelete: 'CASCADE',
})
submission: Submission;
@ManyToOne(() => Question, { onDelete: 'CASCADE' })
question: Question;
@ManyToOne(() => Choice, { nullable: true, onDelete: 'CASCADE' })
choice?: Choice;
@Column({ type: 'int', nullable: true })
numericValue?: number;
@Column({
type: 'varchar',
length: 255,
nullable: true,
})
textValue?: string;
}
@@ -0,0 +1,32 @@
import {
Check,
Column,
CreateDateColumn,
Entity,
ManyToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { Submission } from './submission.entity';
import { ResultRange } from '@/modules/scoring/entities/result-range.entity';
@Entity()
@Check(`"probability" >= 0 AND "probability" <= 100`)
export class SubmissionResult {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => Submission, { onDelete: 'CASCADE' })
submission: Submission;
@Column({ type: 'int', nullable: false })
score: number;
@Column({ type: 'int', nullable: false })
probability: number;
@ManyToOne(() => ResultRange, { onDelete: 'CASCADE' })
resultRange: ResultRange;
@CreateDateColumn()
createdAt: Date;
}
@@ -0,0 +1,37 @@
import { Test } from '@/modules/tests/entities/test.entity';
import { User } from '@/modules/users/entities/user.entity';
import {
Column,
CreateDateColumn,
Entity,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
} from 'typeorm';
import { SubmissionStatus } from '../enums/submission-status.enum';
import { SubmissionAnswer } from './submission-answer.entity';
@Entity()
export class Submission {
@PrimaryGeneratedColumn()
id: number;
@ManyToOne(() => User, { nullable: true, onDelete: 'SET NULL' })
user?: User;
@ManyToOne(() => Test, { onDelete: 'CASCADE' })
test: Test;
@Column({
type: 'enum',
enum: SubmissionStatus,
default: SubmissionStatus.IN_PROGRESS,
})
status: SubmissionStatus;
@OneToMany(() => SubmissionAnswer, (answer) => answer.submission)
answers: Array<SubmissionAnswer>;
@CreateDateColumn()
createdAt: Date;
}
@@ -0,0 +1,4 @@
export enum SubmissionStatus {
IN_PROGRESS = 'in-progress',
COMPLETED = 'completed',
}
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Submission } from './entities/submission.entity';
import { SubmissionAnswer } from './entities/submission-answer.entity';
import { SubmissionResult } from './entities/submission-result.entity';
@Module({
imports: [
TypeOrmModule.forFeature([Submission, SubmissionAnswer, SubmissionResult]),
],
})
export class SubmissionsModule {}
+62
View File
@@ -0,0 +1,62 @@
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
} from 'typeorm';
import { AccessType } from '../enums/access-types.enum';
import { Question } from '@/modules/questions/entities/question.entity';
import { ResultRange } from '@/modules/scoring/entities/result-range.entity';
import { RuleSet } from '@/modules/scoring/entities/rule-set.entity';
@Entity()
export class Test {
@PrimaryGeneratedColumn()
id: number;
@Column({
type: 'varchar',
length: 255,
nullable: false,
})
title: string;
@Column({
type: 'text',
nullable: false,
})
description: string;
@Column({
type: 'enum',
enum: AccessType,
default: AccessType.LOGIN,
})
accessType: AccessType;
@Column({
type: 'bigint',
nullable: true,
})
price: string;
@Column({
type: 'boolean',
default: false,
})
isActive: boolean;
@OneToMany(() => Question, (question) => question.test)
questions: Array<Question>;
@OneToOne(() => RuleSet, (ruleSet) => ruleSet.test)
ruleSet: RuleSet;
@OneToMany(() => ResultRange, (range) => range.test)
resultRanges: Array<ResultRange>;
@CreateDateColumn()
createdAt: Date;
}
@@ -0,0 +1,5 @@
export enum AccessType {
FREE = 'free',
LOGIN = 'login',
PAID = 'paid',
}
+8
View File
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Test } from './entities/test.entity';
@Module({
imports: [TypeOrmModule.forFeature([Test])],
})
export class TestsModule {}
@@ -1,3 +1,4 @@
import { Role } from '@/common/enums/roles.enum';
import {
Column,
CreateDateColumn,
@@ -42,6 +43,13 @@ export class User {
})
password?: string;
@Column({
type: 'enum',
enum: Role,
default: Role.USER,
})
role: Role;
@CreateDateColumn()
createdAt: Date;
@@ -1,7 +1,7 @@
import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider';
import { Injectable, RequestTimeoutException } from '@nestjs/common';
import { I18nService } from 'nestjs-i18n';
import { User } from '../user.entity';
import { User } from '../entities/user.entity';
import { UpdateProfileDTO } from '../dto/update-profile.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
+1 -1
View File
@@ -4,7 +4,7 @@ import {
RequestTimeoutException,
} from '@nestjs/common';
import { Repository } from 'typeorm';
import { User } from '../user.entity';
import { User } from '../entities/user.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { UpdateProfileDTO } from '../dto/update-profile.dto';
import { UserProfileProvider } from './user-profile.provider';
+1 -1
View File
@@ -1,7 +1,7 @@
import { Body, Controller, Get, Patch } from '@nestjs/common';
import { UsersService } from './providers/users.service';
import { ActiveUser } from '@/common/decorators/active-user.decorator';
import { User } from './user.entity';
import { User } from './entities/user.entity';
import { UpdateProfileDTO } from './dto/update-profile.dto';
@Controller('users')
+1 -1
View File
@@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './providers/users.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
import { User } from './entities/user.entity';
import { HashingModule } from '@/common/modules/hashing/hashing.module';
import { UserProfileProvider } from './providers/user-profile.provider';