2nd commit, question added, not tested
This commit is contained in:
parent
9a20d97745
commit
11971398cb
|
|
@ -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[];
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateQuestionDto } from './create-question.dto';
|
||||
|
||||
export class UpdateQuestionDto extends PartialType(CreateQuestionDto) {}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
},
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
|
|||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from './../src/app.module';
|
||||
import { AppModule } from '../src/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
|
|
|||
Loading…
Reference in New Issue