From a74932f8bb8a3fdf89f6841f1e7c4ae337d51018 Mon Sep 17 00:00:00 2001 From: OkaykOrhmn Date: Mon, 14 Jul 2025 08:56:58 +0330 Subject: [PATCH] attachment done --- src/_core/dto/base.dto.ts | 18 +++ src/_core/dto/metadataEntry.dto.ts | 17 +++ src/_core/{ => entity}/_base.entity.ts | 0 src/_core/{ => entity}/_metadata.entity.ts | 0 src/attachment/attachment.controller.ts | 47 ++++++++ src/attachment/attachment.module.ts | 16 +++ src/attachment/attachment.service.ts | 107 ++++++++++++++++++ src/attachment/dto/create_attachment.dto.ts | 21 ++++ src/attachment/dto/update_attachment.dto.ts | 4 + src/attachment/entity/attachment.entity.ts | 2 +- src/option/dto/create-option.dto.ts | 26 +---- src/option/entity/option.entity.ts | 2 +- src/option/option.controller.ts | 12 +- src/participant/dto/create-participant.dto.ts | 26 +---- src/participant/entity/participant.entity.ts | 2 +- src/question/dto/create-question.dto.ts | 83 +------------- src/question/entity/question.entity.ts | 2 +- 17 files changed, 253 insertions(+), 132 deletions(-) create mode 100644 src/_core/dto/base.dto.ts create mode 100644 src/_core/dto/metadataEntry.dto.ts rename src/_core/{ => entity}/_base.entity.ts (100%) rename src/_core/{ => entity}/_metadata.entity.ts (100%) create mode 100644 src/attachment/attachment.controller.ts create mode 100644 src/attachment/attachment.module.ts create mode 100644 src/attachment/attachment.service.ts create mode 100644 src/attachment/dto/create_attachment.dto.ts create mode 100644 src/attachment/dto/update_attachment.dto.ts diff --git a/src/_core/dto/base.dto.ts b/src/_core/dto/base.dto.ts new file mode 100644 index 0000000..129c982 --- /dev/null +++ b/src/_core/dto/base.dto.ts @@ -0,0 +1,18 @@ +import { IsString, IsEnum, IsOptional, IsUUID, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import {MetadataDto} from './metadataEntry.dto'; + +export class BaseDto { + @IsUUID() + @IsOptional() // id is optional since BaseEntity generates it + id?: string; + + @IsEnum(['active', 'inactive']) + @IsOptional() + status?: 'active'| 'inactive'; + + @ValidateNested() + @Type(() => MetadataDto) + @IsOptional() + metadata?: MetadataDto; // Optional, defaults to { entries: [] } +} \ No newline at end of file diff --git a/src/_core/dto/metadataEntry.dto.ts b/src/_core/dto/metadataEntry.dto.ts new file mode 100644 index 0000000..1e895e3 --- /dev/null +++ b/src/_core/dto/metadataEntry.dto.ts @@ -0,0 +1,17 @@ +import { IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; + +class MetadataEntryDto { + @IsString() + key: string; + + @IsString() + value: string; +} + +export class MetadataDto { + @ValidateNested({ each: true }) + @Type(() => MetadataEntryDto) + entries: MetadataEntryDto[]; +} + diff --git a/src/_core/_base.entity.ts b/src/_core/entity/_base.entity.ts similarity index 100% rename from src/_core/_base.entity.ts rename to src/_core/entity/_base.entity.ts diff --git a/src/_core/_metadata.entity.ts b/src/_core/entity/_metadata.entity.ts similarity index 100% rename from src/_core/_metadata.entity.ts rename to src/_core/entity/_metadata.entity.ts diff --git a/src/attachment/attachment.controller.ts b/src/attachment/attachment.controller.ts new file mode 100644 index 0000000..ad93f2a --- /dev/null +++ b/src/attachment/attachment.controller.ts @@ -0,0 +1,47 @@ +import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus,} from '@nestjs/common'; +import { AttachmentService } from './attachment.service'; +import { CreateAttachmentDto } from './dto/create_attachment.dto'; +import { UpdateAttachmentDto } from './dto/update_attachment.dto'; +import { Attachment } from './entity/attachment.entity'; + +@Controller('attachments') +export class AttachmentController { + constructor(private readonly attachmentService: AttachmentService) {} + + @Post() + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + async create(@Body() body: CreateAttachmentDto): Promise { + return this.attachmentService.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.attachmentService.findAll(parseInt(page, 10), parseInt(limit, 10)); + } + + @Get(':id') + async findOne( + @Param('id', new ParseUUIDPipe()) id: string, + ): Promise { + return this.attachmentService.findById(id); + } + + @Patch(':id') + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) + async update( + @Param('id', new ParseUUIDPipe()) id: string, + @Body() body: UpdateAttachmentDto, + ): Promise { + return this.attachmentService.update(id, body); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise { + return this.attachmentService.remove(id); + } +} diff --git a/src/attachment/attachment.module.ts b/src/attachment/attachment.module.ts new file mode 100644 index 0000000..7b0c053 --- /dev/null +++ b/src/attachment/attachment.module.ts @@ -0,0 +1,16 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Attachment, AttachmentSchema } from './entity/attachment.entity'; +import { AttachmentController } from './attachment.controller'; +import { AttachmentService } from './attachment.service'; + +@Module({ + imports: [ + MongooseModule.forFeature([ + { name: Attachment.name, schema: AttachmentSchema }, + ]), + ], + controllers: [AttachmentController], + providers: [AttachmentService], +}) +export class AttachmentModule {} diff --git a/src/attachment/attachment.service.ts b/src/attachment/attachment.service.ts new file mode 100644 index 0000000..e8d9aab --- /dev/null +++ b/src/attachment/attachment.service.ts @@ -0,0 +1,107 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +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'; + +// 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 { + constructor( + @InjectModel(Attachment.name) + private readonly attachmentModel: PaginateModel, + ) {} + + async create(data: CreateAttachmentDto): Promise { + const attachment = new this.attachmentModel({ + ...data, + id: data.id || undefined, // Let BaseEntity generate UUID if not provided + metadata: data.metadata || { entries: [] }, + }); + return attachment.save(); + } + + async findAll( + page = 1, + limit = 10, + ): Promise<{ + docs: Attachment[]; + totalDocs: number; + limit: number; + page: number; + totalPages: number; + hasNextPage: boolean; + hasPrevPage: boolean; + nextPage: number | null; + 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 }, + ); + } + + 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) { + throw new NotFoundException(`Attachment with ID "${id}" not found`); + } + return attachment; + } + + async update( + 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(); + + if (!updatedAttachment) { + throw new NotFoundException(`Attachment with ID "${id}" not found`); + } + return updatedAttachment; + } + + async remove(id: string): Promise { + const result = await this.attachmentModel.deleteOne({ id }).exec(); + if (result.deletedCount === 0) { + throw new NotFoundException(`Attachment with ID "${id}" not found`); + } + } +} diff --git a/src/attachment/dto/create_attachment.dto.ts b/src/attachment/dto/create_attachment.dto.ts new file mode 100644 index 0000000..23f7bb1 --- /dev/null +++ b/src/attachment/dto/create_attachment.dto.ts @@ -0,0 +1,21 @@ +// DTO for embedded Attachment +import { isEnum, IsEnum, IsOptional, IsString, IsUrl, IsUUID, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { BaseDto } from '../../_core/dto/base.dto'; + +export class CreateAttachmentDto extends BaseDto{ + @IsString() + displayName: string; + + @IsString() + filePath: string; + + @IsEnum(['png','pdf','csv','jpg']) + fileType: 'png'|'pdf'|'csv'|'jpg'; + + @IsUrl() + storageUrl: string; + + @IsUUID() + ownerId: string; +} \ No newline at end of file diff --git a/src/attachment/dto/update_attachment.dto.ts b/src/attachment/dto/update_attachment.dto.ts new file mode 100644 index 0000000..134ab85 --- /dev/null +++ b/src/attachment/dto/update_attachment.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateAttachmentDto } from './create_attachment.dto'; + +export class UpdateAttachmentDto extends PartialType(CreateAttachmentDto) {} diff --git a/src/attachment/entity/attachment.entity.ts b/src/attachment/entity/attachment.entity.ts index b500af5..2550d45 100644 --- a/src/attachment/entity/attachment.entity.ts +++ b/src/attachment/entity/attachment.entity.ts @@ -1,6 +1,6 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; -import { BaseEntity } from 'src/_core/_base.entity'; +import { BaseEntity } from 'src/_core/entity/_base.entity'; @Schema({ _id: false, id: false }) // Embedded document, inheriting from BaseEntity export class Attachment extends BaseEntity { diff --git a/src/option/dto/create-option.dto.ts b/src/option/dto/create-option.dto.ts index abc28b4..671c3e5 100644 --- a/src/option/dto/create-option.dto.ts +++ b/src/option/dto/create-option.dto.ts @@ -1,26 +1,8 @@ -import { IsString, IsOptional, ValidateNested } from 'class-validator'; -import { Type } from 'class-transformer'; +import { IsString, IsOptional, ValidateNested, IsUUID, IsEnum } from 'class-validator'; +import { BaseDto } from '../../_core/dto/base.dto'; -class MetadataEntryDto { - @IsString() - key: string; - - @IsString() - value: string; -} - -class MetadataDto { - @ValidateNested({ each: true }) - @Type(() => MetadataEntryDto) - entries: MetadataEntryDto[]; -} - -export class CreateOptionDto { +// DTO for embedded Option +export class CreateOptionDto extends BaseDto{ @IsString() text: string; - - @ValidateNested() - @Type(() => MetadataDto) - @IsOptional() - metadata?: MetadataDto; } \ No newline at end of file diff --git a/src/option/entity/option.entity.ts b/src/option/entity/option.entity.ts index f8a680e..07481e4 100644 --- a/src/option/entity/option.entity.ts +++ b/src/option/entity/option.entity.ts @@ -1,6 +1,6 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; -import { BaseEntity } from 'src/_core/_base.entity'; +import { BaseEntity } from 'src/_core/entity/_base.entity'; @Schema({ _id: false, id: false }) // Embedded document, inheriting from BaseEntity export class Option extends BaseEntity { diff --git a/src/option/option.controller.ts b/src/option/option.controller.ts index 390c499..3b8120d 100644 --- a/src/option/option.controller.ts +++ b/src/option/option.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe } from '@nestjs/common'; +import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query } from '@nestjs/common'; import { ParseUUIDPipe } from '@nestjs/common'; import { OptionService } from './option.service'; import { CreateOptionDto } from './dto/create-option.dto'; @@ -16,9 +16,13 @@ export class OptionController { return this.optionService.create(body); } - @Get() - async findAll(@Paginate() query: PaginateQuery): Promise> { - return this.optionService.findAll(query); + @Get('findAll') + async findAll( + @Query('page') page: string = '1', + @Query('limit') limit: string = '10', + ) { + // The service returns the full pagination object + return this.optionService.findAll(parseInt(page, 10), parseInt(limit, 10)); } @Get(':id') diff --git a/src/participant/dto/create-participant.dto.ts b/src/participant/dto/create-participant.dto.ts index 9568cf6..d1459ee 100644 --- a/src/participant/dto/create-participant.dto.ts +++ b/src/participant/dto/create-participant.dto.ts @@ -1,25 +1,9 @@ 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'; -class MetadataEntryDto { - @IsString() - key: string; - - @IsString() - value: string; -} - -class MetadataDto { - @ValidateNested({ each: true }) - @Type(() => MetadataEntryDto) - entries: MetadataEntryDto[]; -} - -export class CreateParticipantDto { - @IsUUID() - @IsOptional() // id is optional since BaseEntity generates it - id?: string; - +export class CreateParticipantDto extends BaseDto{ @IsUUID() userId: string; // Required, provided by client @@ -30,8 +14,4 @@ export class CreateParticipantDto { @IsOptional() role?: 'moderator' | 'tester' | 'admin' | 'user'; // Optional, defaults to 'user' - @ValidateNested() - @Type(() => MetadataDto) - @IsOptional() - metadata?: MetadataDto; // Optional, defaults to { entries: [] } } \ No newline at end of file diff --git a/src/participant/entity/participant.entity.ts b/src/participant/entity/participant.entity.ts index e6d7842..36919f9 100644 --- a/src/participant/entity/participant.entity.ts +++ b/src/participant/entity/participant.entity.ts @@ -1,6 +1,6 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { v4 as uuidv4 } from 'uuid'; -import { BaseEntity } from 'src/_core/_base.entity'; +import { BaseEntity } from 'src/_core/entity/_base.entity'; import * as mongoosePaginate from 'mongoose-paginate-v2'; import { Document } from 'mongoose'; diff --git a/src/question/dto/create-question.dto.ts b/src/question/dto/create-question.dto.ts index ec79fc2..d9a1b8f 100644 --- a/src/question/dto/create-question.dto.ts +++ b/src/question/dto/create-question.dto.ts @@ -1,81 +1,11 @@ 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'; -} +import {CreateAttachmentDto} from '../../attachment/dto/create_attachment.dto'; +import { BaseDto } from '../../_core/dto/base.dto'; +import { CreateOptionDto } from '../../option/dto/create-option.dto'; // Main DTO for creating a Question -export class CreateQuestionDto { - @IsOptional() - @IsUUID() - id?: string; - +export class CreateQuestionDto extends BaseDto{ @IsString() text: string; @@ -94,11 +24,6 @@ export class CreateQuestionDto { @IsString() validationRules?: string; - @IsOptional() - @ValidateNested() - @Type(() => MetadataDto) - metadata?: MetadataDto; - @IsOptional() @IsArray() @ValidateNested({ each: true }) diff --git a/src/question/entity/question.entity.ts b/src/question/entity/question.entity.ts index c4b9ef0..e862454 100644 --- a/src/question/entity/question.entity.ts +++ b/src/question/entity/question.entity.ts @@ -1,6 +1,6 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; -import { BaseEntity } from 'src/_core/_base.entity'; +import { BaseEntity } from 'src/_core/entity/_base.entity'; import * as mongoosePaginate from 'mongoose-paginate-v2'; import { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity'; import { Option, OptionSchema } from '../../option/entity/option.entity';