diff --git a/src/answer/answer.controller.ts b/src/answer/answer.controller.ts new file mode 100644 index 0000000..5b0795d --- /dev/null +++ b/src/answer/answer.controller.ts @@ -0,0 +1,47 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus,} from '@nestjs/common'; +import { AnswerService } from './answer.service'; +import { CreateAnswerDto } from './dto/create-answer.dto'; +import { UpdateAnswerDto } from './dto/update-answer.dto'; +import { Answer } from './entity/answer.entity'; + +@Controller('answers') +export class AnswerController { + constructor(private readonly answerService: AnswerService) {} + + @Post() + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + async create(@Body() body: CreateAnswerDto): Promise { + return this.answerService.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.answerService.findAll(parseInt(page, 10), parseInt(limit, 10)); + } + + @Get(':id') + async findOne( + @Param('id', new ParseUUIDPipe()) id: string, + ): Promise { + return this.answerService.findById(id); + } + + @Patch(':id') + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + async update( + @Param('id', new ParseUUIDPipe()) id: string, + @Body() body: UpdateAnswerDto, + ): Promise { + return this.answerService.update(id, body); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise { + return this.answerService.remove(id); + } +} diff --git a/src/answer/answer.module.ts b/src/answer/answer.module.ts new file mode 100644 index 0000000..830cc17 --- /dev/null +++ b/src/answer/answer.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { AnswerService } from './answer.service'; +import { AnswerController } from './answer.controller'; +import { Answer, AnswerSchema } from './entity/answer.entity'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { name: Answer.name, schema: AnswerSchema }, + ]), + ], + controllers: [AnswerController], + providers: [AnswerService], +}) +export class AnswerModule {} diff --git a/src/answer/answer.service.ts b/src/answer/answer.service.ts new file mode 100644 index 0000000..045606a --- /dev/null +++ b/src/answer/answer.service.ts @@ -0,0 +1,81 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { Answer, AnswerDocument } from './entity/answer.entity'; +import { CreateAnswerDto } from './dto/create-answer.dto'; +import { UpdateAnswerDto } from './dto/update-answer.dto'; +import { PaginateModel } from '../participant/participant.service'; + +@Injectable() +export class AnswerService { + constructor( + @InjectModel(Answer.name) + private readonly answerModel: PaginateModel, + ) {} + + async create(data: CreateAnswerDto): Promise { + const answer = new this.answerModel({ + ...data, + id: data.id || undefined, // Let BaseEntity generate UUID if not provided + metadata: data.metadata || { entries: [] }, + }); + return answer.save(); + } + + async findAll( + page = 1, + limit = 10, + ): Promise<{ + docs: Answer[]; + totalDocs: number; + limit: number; + page: number; + totalPages: number; + hasNextPage: boolean; + hasPrevPage: boolean; + nextPage: number | null; + prevPage: number | null; + }> { + return this.answerModel.paginate( + {}, + { page, limit, lean: true }, + ); + } + + async findById(id: string): Promise { + const answer = await this.answerModel + .findOne({ id }) + .lean() + .exec(); + if (!answer) { + throw new NotFoundException(`Answer with ID "${id}" not found`); + } + return answer; + } + + async update( + id: string, + data: UpdateAnswerDto, + ): Promise { + const updatedAnswer = await this.answerModel + .findOneAndUpdate( + { id }, + { $set: { ...data, updatedAt: new Date() } }, + { new: true }, + ) + .lean() + .exec(); + + if (!updatedAnswer) { + throw new NotFoundException(`Answer with ID "${id}" not found`); + } + return updatedAnswer; + } + + async remove(id: string): Promise { + const result = await this.answerModel.deleteOne({ id }).exec(); + if (result.deletedCount === 0) { + throw new NotFoundException(`Answer with ID "${id}" not found`); + } + } +} diff --git a/src/answer/dto/create-answer.dto.ts b/src/answer/dto/create-answer.dto.ts new file mode 100644 index 0000000..d2513b2 --- /dev/null +++ b/src/answer/dto/create-answer.dto.ts @@ -0,0 +1,13 @@ +import { IsString, IsUUID} from 'class-validator'; +import { BaseDto } from '../../_core/dto/base.dto'; + +export class CreateAnswerDto extends BaseDto{ + @IsUUID() + participantId: string; + + @IsUUID() + questionId: string; + + @IsString() + value: string; +} diff --git a/src/answer/dto/update-answer.dto.ts b/src/answer/dto/update-answer.dto.ts new file mode 100644 index 0000000..f073f58 --- /dev/null +++ b/src/answer/dto/update-answer.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateAnswerDto } from './create-answer.dto'; + +export class UpdateAnswerDto extends PartialType(CreateAnswerDto) {} diff --git a/src/answer/entity/answer.entity.ts b/src/answer/entity/answer.entity.ts new file mode 100644 index 0000000..b735b40 --- /dev/null +++ b/src/answer/entity/answer.entity.ts @@ -0,0 +1,44 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; +import { BaseEntity } from 'src/_core/entity/_base.entity'; +import * as mongoosePaginate from 'mongoose-paginate-v2'; +import { v4 as uuidv4 } from 'uuid'; + +@Schema({ _id: false, id: false }) // Disable _id and virtual id +export class Answer extends BaseEntity { + @Prop({ + type: String, + default: () => uuidv4(), + required: true, + immutable: true, + }) + participantId: string; + + @Prop({ + type: String, + default: () => uuidv4(), + required: true, + immutable: true, + }) + questionId: string; // form can be identified based on question + + @Prop({ + type: String, + required: true, + }) + value: string; //value can be mapped to option based on question type + + +} + +export type AnswerDocument = Answer & Document; +export const AnswerSchema = SchemaFactory.createForClass(Answer); +AnswerSchema.plugin(mongoosePaginate); + +// Transform the output to remove the internal '_id' +AnswerSchema.set('toJSON', { + transform: (doc: AnswerDocument, ret: Answer & { _id?: any }) => { + delete ret._id; + return ret; + }, +}); diff --git a/src/attachment/attachment.service.ts b/src/attachment/attachment.service.ts index e8d9aab..30188a0 100644 --- a/src/attachment/attachment.service.ts +++ b/src/attachment/attachment.service.ts @@ -4,24 +4,8 @@ import { Model } from 'mongoose'; import { Attachment, AttachmentDocument } from './entity/attachment.entity'; import { CreateAttachmentDto } from './dto/create_attachment.dto'; import { UpdateAttachmentDto } from './dto/update_attachment.dto'; +import { PaginateModel } from '../participant/participant.service'; -// Interface matching the mongoose-paginate-v2 plugin -interface PaginateModel extends Model { - 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 AttachmentService { @@ -54,20 +38,15 @@ export class AttachmentService { prevPage: number | null; }> { // Selects all fields from the Attachment entity for the response - const selectFields = - 'id text type isRequired isConfidential validationRules options attachments metadata status createdAt updatedAt'; return this.attachmentModel.paginate( {}, - { page, limit, lean: true, select: selectFields }, + { page, limit, lean: true }, ); } async findById(id: string): Promise { - const selectFields = - 'id text type isRequired isConfidential validationRules options attachments metadata status createdAt updatedAt'; const attachment = await this.attachmentModel .findOne({ id }) - .select(selectFields) .lean() .exec(); if (!attachment) { @@ -80,15 +59,12 @@ export class AttachmentService { id: string, data: UpdateAttachmentDto, ): Promise { - const selectFields = - 'id text type isRequired isConfidential validationRules options attachments metadata status createdAt updatedAt'; const updatedAttachment = await this.attachmentModel .findOneAndUpdate( { id }, { $set: { ...data, updatedAt: new Date() } }, { new: true }, ) - .select(selectFields) .lean() .exec(); diff --git a/src/option/option.service.ts b/src/option/option.service.ts index cdd6007..27c567b 100644 --- a/src/option/option.service.ts +++ b/src/option/option.service.ts @@ -1,28 +1,10 @@ import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; import { CreateOptionDto } from './dto/create-option.dto'; import { UpdateOptionDto } from './dto/update-option.dto'; import { Option, OptionDocument } from './entity/option.entity'; -import { PaginateQuery, Paginated, paginate } from 'nestjs-paginate'; +import { PaginateModel } from '../participant/participant.service'; - -interface PaginateModel extends Model { - 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 OptionService { constructor( @@ -48,7 +30,7 @@ export class OptionService { }> { return this.optionModel.paginate( {}, - { page, limit, lean: true, select: 'id metadata status text createdAt updatedAt' }, + { page, limit, lean: true }, ); } diff --git a/src/participant/participant.service.ts b/src/participant/participant.service.ts index b4e0cef..85ce749 100644 --- a/src/participant/participant.service.ts +++ b/src/participant/participant.service.ts @@ -6,7 +6,7 @@ import { Participant, ParticipantDocument } from './entity/participant.entity'; import { UpdateParticipantDto } from './dto/update-participant.dto'; import { v4 as uuidv4 } from 'uuid'; -interface PaginateModel extends Model { +export interface PaginateModel extends Model { paginate: ( query?: any, options?: { page: number; limit: number; lean?: boolean; select?: string }, @@ -52,12 +52,12 @@ export class ParticipantService { }> { return this.participantModel.paginate( {}, - { page, limit, lean: true, select: 'id metadata status userId role displayName createdAt updatedAt' }, + { page, limit, lean: true}, ); } async findById(id: string): Promise { - return this.participantModel.findOne({ id }).select('id metadata status userId role displayName createdAt updatedAt').lean().exec(); + return this.participantModel.findOne({ id }).lean().exec(); } async update(id: string, data: UpdateParticipantDto): Promise { @@ -66,10 +66,7 @@ export class ParticipantService { { id }, { $set: { ...data, updatedAt: new Date() } }, { new: true }, - ) - .select('id metadata status userId role displayName createdAt updatedAt') - .lean() - .exec(); + ).lean().exec(); } async remove(id: string): Promise { diff --git a/src/participantGroup/dto/create-participantGroup.dto.ts b/src/participantGroup/dto/create-participantGroup.dto.ts new file mode 100644 index 0000000..312f6d0 --- /dev/null +++ b/src/participantGroup/dto/create-participantGroup.dto.ts @@ -0,0 +1,12 @@ +import { IsString, IsEnum, IsOptional, IsUUID, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { MetadataDto } from '../../_core/dto/metadataEntry.dto'; +import { BaseDto } from '../../_core/dto/base.dto'; + +export class CreateParticipantGroupDto extends BaseDto{ + @IsString() + title: string; // Required, provided by client + + @IsEnum(['confidential','delphi','one-time-question','one-time-section','one-time-page','one-time-form']) + type: 'confidential'|'delphi'|'one-time-question'|'one-time-section'|'one-time-page'|'one-time-form'; +} \ No newline at end of file diff --git a/src/participantGroup/dto/update-participantGroup.dto.ts b/src/participantGroup/dto/update-participantGroup.dto.ts new file mode 100644 index 0000000..610d1b1 --- /dev/null +++ b/src/participantGroup/dto/update-participantGroup.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateParticipantGroupDto } from './create-participantGroup.dto'; + +export class UpdateParticipantGroupDto extends PartialType(CreateParticipantGroupDto) {} diff --git a/src/participantGroup/entity/participantGroup.entity.ts b/src/participantGroup/entity/participantGroup.entity.ts new file mode 100644 index 0000000..7ca69a0 --- /dev/null +++ b/src/participantGroup/entity/participantGroup.entity.ts @@ -0,0 +1,31 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { BaseEntity } from 'src/_core/entity/_base.entity'; +import * as mongoosePaginate from 'mongoose-paginate-v2'; +import { Document } from 'mongoose'; + +@Schema({ _id: false, id: false }) // Disable _id and virtual id +export class ParticipantGroup extends BaseEntity { + @Prop({ + type: String, + required: true, + }) + title: string; + + @Prop({ + type: String, + enum: ['confidential','delphi','one-time-question','one-time-section','one-time-page','one-time-form'], + // default: '', + required: true, + }) + type: 'confidential'|'delphi'|'one-time-question'|'one-time-section'|'one-time-page'|'one-time-form'; +} + +export type ParticipantGroupDocument = ParticipantGroup & Document; +export const ParticipantGroupSchema = SchemaFactory.createForClass(ParticipantGroup); +ParticipantGroupSchema.plugin(mongoosePaginate); +ParticipantGroupSchema.set('toJSON', { + transform: (doc: ParticipantGroupDocument, ret: ParticipantGroup & { _id?: any }) => { + delete ret._id; + return ret; + }, +}); \ No newline at end of file diff --git a/src/participantGroup/participantGroup.controller.ts b/src/participantGroup/participantGroup.controller.ts new file mode 100644 index 0000000..f74a9b1 --- /dev/null +++ b/src/participantGroup/participantGroup.controller.ts @@ -0,0 +1,53 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe } from '@nestjs/common'; +import { ParticipantGroupService } from './participantGroup.service'; +import { CreateParticipantGroupDto } from './dto/create-participantGroup.dto'; +import { UpdateParticipantGroupDto } from './dto/update-participantGroup.dto'; +import { ParticipantGroup } from './entity/participantGroup.entity'; + +@Controller('participantGroupGroups') +export class ParticipantGroupController { + constructor(private readonly participantGroupService: ParticipantGroupService) {} + + @Post() + @UsePipes(new ValidationPipe({ transform: true })) + async create(@Body() body: CreateParticipantGroupDto): Promise { + return this.participantGroupService.create(body); + } + + @Get('findAll') // returns non uuid id !!! + async findAll( + @Query('page') page: string = '1', + @Query('limit') limit: string = '10', + ): Promise<{ + docs: ParticipantGroup[]; + totalDocs: number; + limit: number; + page: number; + totalPages: number; + hasNextPage: boolean; + hasPrevPage: boolean; + nextPage: number | null; + prevPage: number | null; + }> { + return this.participantGroupService.findAll(parseInt(page), parseInt(limit)); + } + + @Get(':id') + async findOne(@Param('id', new ParseUUIDPipe()) id: string): Promise { + return this.participantGroupService.findById(id); + } + + @Patch(':id') + @UsePipes(new ValidationPipe({ transform: true })) + async update( + @Param('id', new ParseUUIDPipe()) id: string, + @Body() body: UpdateParticipantGroupDto, + ): Promise { + return this.participantGroupService.update(id, body); + } + + @Delete(':id') + async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise { + return this.participantGroupService.remove(id); + } +} \ No newline at end of file diff --git a/src/participantGroup/participantGroup.module.ts b/src/participantGroup/participantGroup.module.ts new file mode 100644 index 0000000..88adc23 --- /dev/null +++ b/src/participantGroup/participantGroup.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { ParticipantGroupService } from './participantGroup.service'; +import { ParticipantGroupController } from './participantGroup.controller'; +import { ParticipantGroup, ParticipantGroupSchema } from './entity/participantGroup.entity'; + +@Module({ + imports: [ + MongooseModule.forFeature([{ name: ParticipantGroup.name, schema: ParticipantGroupSchema }]), + ], + controllers: [ParticipantGroupController], + providers: [ParticipantGroupService], +}) +export class ParticipanGrouptModule {} \ No newline at end of file diff --git a/src/participantGroup/participantGroup.service.ts b/src/participantGroup/participantGroup.service.ts new file mode 100644 index 0000000..7877e7e --- /dev/null +++ b/src/participantGroup/participantGroup.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { CreateParticipantGroupDto } from './dto/create-participantGroup.dto'; +import { ParticipantGroup, ParticipantGroupDocument } from './entity/participantGroup.entity'; +import { UpdateParticipantGroupDto } from './dto/update-participantGroup.dto'; +import { v4 as uuidv4 } from 'uuid'; +import { PaginateModel } from '../participant/participant.service'; + + +@Injectable() +export class ParticipantGroupService { + constructor( + @InjectModel(ParticipantGroup.name) + private readonly participantGroupModel: PaginateModel, + ) {} + + async create(data: CreateParticipantGroupDto): Promise { + const participantGroup = new this.participantGroupModel({ + ...data, + id: data.id || undefined, // Let BaseEntity generate UUID + metadata: data.metadata || { entries: [] }, + }); + return participantGroup.save(); + } + + async findAll(page = 1, limit = 10): Promise<{ // returns non uuid id !!! + docs: ParticipantGroup[]; + totalDocs: number; + limit: number; + page: number; + totalPages: number; + hasNextPage: boolean; + hasPrevPage: boolean; + nextPage: number | null; + prevPage: number | null; + }> { + return this.participantGroupModel.paginate( + {}, + { page, limit, lean: true }, + ); + } + + async findById(id: string): Promise { + return this.participantGroupModel.findOne({ id }).lean().exec(); + } + + async update(id: string, data: UpdateParticipantGroupDto): Promise { + return this.participantGroupModel + .findOneAndUpdate( + { id }, + { $set: { ...data, updatedAt: new Date() } }, + { new: true }, + ).lean().exec(); + } + + async remove(id: string): Promise { + await this.participantGroupModel.deleteOne({ id }).exec(); + } +} \ No newline at end of file diff --git a/src/question/question.service.ts b/src/question/question.service.ts index a2230b8..a0eee9e 100644 --- a/src/question/question.service.ts +++ b/src/question/question.service.ts @@ -4,24 +4,7 @@ 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 extends Model { - 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; - }>; -} +import { PaginateModel } from '../participant/participant.service'; @Injectable() export class QuestionService { @@ -54,20 +37,15 @@ export class QuestionService { 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 }, + { page, limit, lean: true }, ); } async findById(id: string): Promise { - 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) { @@ -80,15 +58,12 @@ export class QuestionService { id: string, data: UpdateQuestionDto, ): Promise { - 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();