2nd commit, question added, not tested

This commit is contained in:
OkaykOrhmn 2025-07-13 15:54:33 +03:30
parent 9a20d97745
commit 11971398cb
9 changed files with 436 additions and 1 deletions

View File

@ -0,0 +1,113 @@
import { IsString, IsBoolean, IsOptional, IsUUID, ValidateNested, IsArray, IsEnum, IsUrl,} from 'class-validator';
import { Type } from 'class-transformer';
// DTO for Metadata entries, consistent with participant DTO
class MetadataEntryDto {
@IsString()
key: string;
@IsString()
value: string;
}
class MetadataDto {
@IsOptional()
@IsUUID()
id?: string;
@ValidateNested({ each: true })
@Type(() => MetadataEntryDto)
entries: MetadataEntryDto[];
}
// DTO for embedded Option
class CreateOptionDto {
@IsOptional()
@IsUUID()
id?: string;
@IsString()
text: string;
@IsOptional()
@ValidateNested()
@Type(() => MetadataDto)
metadata?: MetadataDto;
@IsOptional()
@IsEnum(['active', 'inactive'])
status?: 'active' | 'inactive';
}
// DTO for embedded Attachment
class CreateAttachmentDto {
@IsOptional()
@IsUUID()
id?: string;
@IsString()
displayName: string;
@IsString()
filePath: string;
@IsString()
fileType: string;
@IsUrl()
storageUrl: string;
@IsUUID()
ownerId: string;
@IsOptional()
@ValidateNested()
@Type(() => MetadataDto)
metadata?: MetadataDto;
@IsOptional()
@IsEnum(['active', 'inactive'])
status?: 'active' | 'inactive';
}
// Main DTO for creating a Question
export class CreateQuestionDto {
@IsOptional()
@IsUUID()
id?: string;
@IsString()
text: string;
@IsEnum(['multiple-choice', 'single-choice', 'text', 'rating', 'file'])
type: string;
@IsOptional()
@IsBoolean()
isRequired?: boolean;
@IsOptional()
@IsBoolean()
isConfidential?: boolean;
@IsOptional()
@IsString()
validationRules?: string;
@IsOptional()
@ValidateNested()
@Type(() => MetadataDto)
metadata?: MetadataDto;
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateOptionDto)
options?: CreateOptionDto[];
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => CreateAttachmentDto)
attachments?: CreateAttachmentDto[];
}

View File

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/mapped-types';
import { CreateQuestionDto } from './create-question.dto';
export class UpdateQuestionDto extends PartialType(CreateQuestionDto) {}

View File

@ -0,0 +1,39 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { BaseEntity } from 'src/_core/_base.entity';
@Schema({ _id: false, id: false }) // Embedded document, inheriting from BaseEntity
export class Attachment extends BaseEntity {
@Prop({
type: String,
required: true,
})
displayName: string;
@Prop({
type: String,
required: true,
})
filePath: string;
@Prop({
type: String,
required: true,
})
fileType: string;
@Prop({
type: String,
required: true,
})
storageUrl: string;
@Prop({
type: String,
required: true,
})
ownerId: string; // UUID of the user who uploaded it
}
export type AttachmentDocument = Attachment & Document;
export const AttachmentSchema = SchemaFactory.createForClass(Attachment);

View File

@ -0,0 +1,15 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { BaseEntity } from 'src/_core/_base.entity';
@Schema({ _id: false, id: false }) // Embedded document, inheriting from BaseEntity
export class Option extends BaseEntity {
@Prop({
type: String,
required: true,
})
text: string;
}
export type OptionDocument = Option & Document;
export const OptionSchema = SchemaFactory.createForClass(Option);

View File

@ -0,0 +1,68 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
import { BaseEntity } from 'src/_core/_base.entity';
import * as mongoosePaginate from 'mongoose-paginate-v2';
import { Attachment, AttachmentSchema } from './attachment.entity';
import { Option, OptionSchema } from './option.entity';
@Schema({ _id: false, id: false }) // Disable _id and virtual id
export class Question extends BaseEntity {
@Prop({
type: String,
required: true,
})
text: string;
@Prop({
type: String,
required: true,
// Example types: 'multiple-choice', 'text', 'rating', etc.
enum: ['multiple-choice', 'single-choice', 'text', 'rating', 'file'],
default: 'text',
})
type: string;
@Prop({
type: Boolean,
default: false,
required: true,
})
isRequired: boolean;
@Prop({
type: Boolean,
default: false,
required: true,
})
isConfidential: boolean;
@Prop({
type: String,
required: false, // Assuming validation rules might not always be present
})
validationRules?: string;
@Prop({
type: [OptionSchema],
default: [],
})
options: Option[];
@Prop({
type: [AttachmentSchema],
default: [],
})
attachments: Attachment[];
}
export type QuestionDocument = Question & Document;
export const QuestionSchema = SchemaFactory.createForClass(Question);
QuestionSchema.plugin(mongoosePaginate);
// Transform the output to remove the internal '_id'
QuestionSchema.set('toJSON', {
transform: (doc: QuestionDocument, ret: Question & { _id?: any }) => {
delete ret._id;
return ret;
},
});

View File

@ -0,0 +1,47 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus,} from '@nestjs/common';
import { QuestionService } from './question.service';
import { CreateQuestionDto } from './dto/create-question.dto';
import { UpdateQuestionDto } from './dto/update-question.dto';
import { Question } from './entity/question.entity';
@Controller('questions')
export class QuestionController {
constructor(private readonly questionService: QuestionService) {}
@Post()
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async create(@Body() body: CreateQuestionDto): Promise<Question> {
return this.questionService.create(body);
}
@Get('findAll')
async findAll(
@Query('page') page: string = '1',
@Query('limit') limit: string = '10',
) {
// The service returns the full pagination object
return this.questionService.findAll(parseInt(page, 10), parseInt(limit, 10));
}
@Get(':id')
async findOne(
@Param('id', new ParseUUIDPipe()) id: string,
): Promise<Question | null> {
return this.questionService.findById(id);
}
@Patch(':id')
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async update(
@Param('id', new ParseUUIDPipe()) id: string,
@Body() body: UpdateQuestionDto,
): Promise<Question | null> {
return this.questionService.update(id, body);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> {
return this.questionService.remove(id);
}
}

View File

@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { QuestionService } from './question.service';
import { QuestionController } from './question.controller';
import { Question, QuestionSchema } from './entity/question.entity';
@Module({
imports: [
MongooseModule.forFeature([
{ name: Question.name, schema: QuestionSchema },
]),
],
controllers: [QuestionController],
providers: [QuestionService],
})
export class QuestionModule {}

View File

@ -0,0 +1,133 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Question, QuestionDocument } from './entity/question.entity';
import { CreateQuestionDto } from './dto/create-question.dto';
import { UpdateQuestionDto } from './dto/update-question.dto';
// Interface matching the mongoose-paginate-v2 plugin
interface PaginateModel<T> extends Model<T> {
paginate: (
query?: any,
options?: { page: number; limit: number; lean?: boolean; select?: string },
) => Promise<{
docs: T[];
totalDocs: number;
limit: number;
page: number;
totalPages: number;
hasNextPage: boolean;
hasPrevPage: boolean;
nextPage: number | null;
prevPage: number | null;
}>;
}
@Injectable()
export class QuestionService {
constructor(
@InjectModel(Question.name)
private readonly questionModel: PaginateModel<QuestionDocument>,
) {}
/**
* Creates a new question.
* @param data - The data to create the question with.
* @returns The newly created question.
*/
async create(data: CreateQuestionDto): Promise<Question> {
const question = new this.questionModel({
...data,
id: data.id || undefined, // Let BaseEntity generate UUID if not provided
metadata: data.metadata || { entries: [] },
});
return question.save();
}
/**
* Finds all questions with pagination.
* @param page - The current page number.
* @param limit - The number of items per page.
* @returns A paginated list of questions.
*/
async findAll(
page = 1,
limit = 10,
): Promise<{
docs: Question[];
totalDocs: number;
limit: number;
page: number;
totalPages: number;
hasNextPage: boolean;
hasPrevPage: boolean;
nextPage: number | null;
prevPage: number | null;
}> {
// Selects all fields from the Question entity for the response
const selectFields =
'id text type isRequired isConfidential validationRules options attachments metadata status createdAt updatedAt';
return this.questionModel.paginate(
{},
{ page, limit, lean: true, select: selectFields },
);
}
/**
* Finds a single question by its UUID.
* @param id - The UUID of the question.
* @returns The found question or null.
*/
async findById(id: string): Promise<Question | null> {
const selectFields =
'id text type isRequired isConfidential validationRules options attachments metadata status createdAt updatedAt';
const question = await this.questionModel
.findOne({ id })
.select(selectFields)
.lean()
.exec();
if (!question) {
throw new NotFoundException(`Question with ID "${id}" not found`);
}
return question;
}
/**
* Updates a question by its UUID.
* @param id - The UUID of the question to update.
* @param data - The data to update the question with.
* @returns The updated question or null.
*/
async update(
id: string,
data: UpdateQuestionDto,
): Promise<Question | null> {
const selectFields =
'id text type isRequired isConfidential validationRules options attachments metadata status createdAt updatedAt';
const updatedQuestion = await this.questionModel
.findOneAndUpdate(
{ id },
{ $set: { ...data, updatedAt: new Date() } },
{ new: true },
)
.select(selectFields)
.lean()
.exec();
if (!updatedQuestion) {
throw new NotFoundException(`Question with ID "${id}" not found`);
}
return updatedQuestion;
}
/**
* Removes a question by its UUID.
* @param id - The UUID of the question to remove.
*/
async remove(id: string): Promise<void> {
const result = await this.questionModel.deleteOne({ id }).exec();
if (result.deletedCount === 0) {
throw new NotFoundException(`Question with ID "${id}" not found`);
}
}
}

View File

@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import * as request from 'supertest'; import * as request from 'supertest';
import { App } from 'supertest/types'; import { App } from 'supertest/types';
import { AppModule } from './../src/app.module'; import { AppModule } from '../src/app.module';
describe('AppController (e2e)', () => { describe('AppController (e2e)', () => {
let app: INestApplication<App>; let app: INestApplication<App>;