answer done

This commit is contained in:
OkaykOrhmn 2025-07-14 09:31:19 +03:30
parent a74932f8bb
commit 89f0fdbb60
16 changed files with 389 additions and 80 deletions

View File

@ -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<Answer> {
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<Answer | null> {
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<Answer | null> {
return this.answerService.update(id, body);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> {
return this.answerService.remove(id);
}
}

View File

@ -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 {}

View File

@ -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<AnswerDocument>,
) {}
async create(data: CreateAnswerDto): Promise<Answer> {
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<Answer | null> {
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<Answer | null> {
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<void> {
const result = await this.answerModel.deleteOne({ id }).exec();
if (result.deletedCount === 0) {
throw new NotFoundException(`Answer with ID "${id}" not found`);
}
}
}

View File

@ -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;
}

View File

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

View File

@ -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;
},
});

View File

@ -4,24 +4,8 @@ import { Model } from 'mongoose';
import { Attachment, AttachmentDocument } from './entity/attachment.entity'; import { Attachment, AttachmentDocument } from './entity/attachment.entity';
import { CreateAttachmentDto } from './dto/create_attachment.dto'; import { CreateAttachmentDto } from './dto/create_attachment.dto';
import { UpdateAttachmentDto } from './dto/update_attachment.dto'; import { UpdateAttachmentDto } from './dto/update_attachment.dto';
import { PaginateModel } from '../participant/participant.service';
// 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() @Injectable()
export class AttachmentService { export class AttachmentService {
@ -54,20 +38,15 @@ export class AttachmentService {
prevPage: number | null; prevPage: number | null;
}> { }> {
// Selects all fields from the Attachment entity for the response // 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( return this.attachmentModel.paginate(
{}, {},
{ page, limit, lean: true, select: selectFields }, { page, limit, lean: true },
); );
} }
async findById(id: string): Promise<Attachment | null> { async findById(id: string): Promise<Attachment | null> {
const selectFields =
'id text type isRequired isConfidential validationRules options attachments metadata status createdAt updatedAt';
const attachment = await this.attachmentModel const attachment = await this.attachmentModel
.findOne({ id }) .findOne({ id })
.select(selectFields)
.lean() .lean()
.exec(); .exec();
if (!attachment) { if (!attachment) {
@ -80,15 +59,12 @@ export class AttachmentService {
id: string, id: string,
data: UpdateAttachmentDto, data: UpdateAttachmentDto,
): Promise<Attachment | null> { ): Promise<Attachment | null> {
const selectFields =
'id text type isRequired isConfidential validationRules options attachments metadata status createdAt updatedAt';
const updatedAttachment = await this.attachmentModel const updatedAttachment = await this.attachmentModel
.findOneAndUpdate( .findOneAndUpdate(
{ id }, { id },
{ $set: { ...data, updatedAt: new Date() } }, { $set: { ...data, updatedAt: new Date() } },
{ new: true }, { new: true },
) )
.select(selectFields)
.lean() .lean()
.exec(); .exec();

View File

@ -1,28 +1,10 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { CreateOptionDto } from './dto/create-option.dto'; import { CreateOptionDto } from './dto/create-option.dto';
import { UpdateOptionDto } from './dto/update-option.dto'; import { UpdateOptionDto } from './dto/update-option.dto';
import { Option, OptionDocument } from './entity/option.entity'; import { Option, OptionDocument } from './entity/option.entity';
import { PaginateQuery, Paginated, paginate } from 'nestjs-paginate'; import { PaginateModel } from '../participant/participant.service';
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() @Injectable()
export class OptionService { export class OptionService {
constructor( constructor(
@ -48,7 +30,7 @@ export class OptionService {
}> { }> {
return this.optionModel.paginate( return this.optionModel.paginate(
{}, {},
{ page, limit, lean: true, select: 'id metadata status text createdAt updatedAt' }, { page, limit, lean: true },
); );
} }

View File

@ -6,7 +6,7 @@ import { Participant, ParticipantDocument } from './entity/participant.entity';
import { UpdateParticipantDto } from './dto/update-participant.dto'; import { UpdateParticipantDto } from './dto/update-participant.dto';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
interface PaginateModel<T> extends Model<T> { export interface PaginateModel<T> extends Model<T> {
paginate: ( paginate: (
query?: any, query?: any,
options?: { page: number; limit: number; lean?: boolean; select?: string }, options?: { page: number; limit: number; lean?: boolean; select?: string },
@ -52,12 +52,12 @@ export class ParticipantService {
}> { }> {
return this.participantModel.paginate( 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<Participant | null> { async findById(id: string): Promise<Participant | null> {
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<Participant | null> { async update(id: string, data: UpdateParticipantDto): Promise<Participant | null> {
@ -66,10 +66,7 @@ export class ParticipantService {
{ id }, { id },
{ $set: { ...data, updatedAt: new Date() } }, { $set: { ...data, updatedAt: new Date() } },
{ new: true }, { new: true },
) ).lean().exec();
.select('id metadata status userId role displayName createdAt updatedAt')
.lean()
.exec();
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {

View File

@ -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';
}

View File

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

View File

@ -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;
},
});

View File

@ -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<ParticipantGroup> {
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<ParticipantGroup | null> {
return this.participantGroupService.findById(id);
}
@Patch(':id')
@UsePipes(new ValidationPipe({ transform: true }))
async update(
@Param('id', new ParseUUIDPipe()) id: string,
@Body() body: UpdateParticipantGroupDto,
): Promise<ParticipantGroup | null> {
return this.participantGroupService.update(id, body);
}
@Delete(':id')
async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> {
return this.participantGroupService.remove(id);
}
}

View File

@ -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 {}

View File

@ -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<ParticipantGroupDocument>,
) {}
async create(data: CreateParticipantGroupDto): Promise<ParticipantGroup> {
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<ParticipantGroup | null> {
return this.participantGroupModel.findOne({ id }).lean().exec();
}
async update(id: string, data: UpdateParticipantGroupDto): Promise<ParticipantGroup | null> {
return this.participantGroupModel
.findOneAndUpdate(
{ id },
{ $set: { ...data, updatedAt: new Date() } },
{ new: true },
).lean().exec();
}
async remove(id: string): Promise<void> {
await this.participantGroupModel.deleteOne({ id }).exec();
}
}

View File

@ -4,24 +4,7 @@ import { Model } from 'mongoose';
import { Question, QuestionDocument } from './entity/question.entity'; import { Question, QuestionDocument } from './entity/question.entity';
import { CreateQuestionDto } from './dto/create-question.dto'; import { CreateQuestionDto } from './dto/create-question.dto';
import { UpdateQuestionDto } from './dto/update-question.dto'; import { UpdateQuestionDto } from './dto/update-question.dto';
import { PaginateModel } from '../participant/participant.service';
// 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() @Injectable()
export class QuestionService { export class QuestionService {
@ -54,20 +37,15 @@ export class QuestionService {
prevPage: number | null; prevPage: number | null;
}> { }> {
// Selects all fields from the Question entity for the response // 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( return this.questionModel.paginate(
{}, {},
{ page, limit, lean: true, select: selectFields }, { page, limit, lean: true },
); );
} }
async findById(id: string): Promise<Question | 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 const question = await this.questionModel
.findOne({ id }) .findOne({ id })
.select(selectFields)
.lean() .lean()
.exec(); .exec();
if (!question) { if (!question) {
@ -80,15 +58,12 @@ export class QuestionService {
id: string, id: string,
data: UpdateQuestionDto, data: UpdateQuestionDto,
): Promise<Question | null> { ): Promise<Question | null> {
const selectFields =
'id text type isRequired isConfidential validationRules options attachments metadata status createdAt updatedAt';
const updatedQuestion = await this.questionModel const updatedQuestion = await this.questionModel
.findOneAndUpdate( .findOneAndUpdate(
{ id }, { id },
{ $set: { ...data, updatedAt: new Date() } }, { $set: { ...data, updatedAt: new Date() } },
{ new: true }, { new: true },
) )
.select(selectFields)
.lean() .lean()
.exec(); .exec();