From 4c3ccaa7b4b456c1607ba737ee8df7b151ca989d Mon Sep 17 00:00:00 2001 From: OkaykOrhmn Date: Tue, 22 Jul 2025 13:05:07 +0330 Subject: [PATCH] mongo to postgres (formResult and realm left to do) --- package-lock.json | 151 ++++++++++++++++-- package.json | 2 + src/_core/dto/base.dto.ts | 8 +- src/_core/dto/metadataEntry.dto.ts | 1 - src/_core/entity/_base.entity.ts | 47 ++---- src/_core/entity/_metadata.entity.ts | 31 ++-- src/answer/answer.controller.ts | 5 +- src/answer/answer.module.ts | 10 +- src/answer/answer.service.ts | 105 +++++++----- src/answer/dto/create-answer.dto.ts | 6 +- src/answer/entity/answer.entity.ts | 48 ++---- src/app.module.ts | 5 +- src/attachment/attachment.controller.ts | 5 +- src/attachment/attachment.module.ts | 10 +- src/attachment/attachment.service.ts | 107 +++++++------ src/attachment/dto/create_attachment.dto.ts | 9 +- src/attachment/entity/attachment.entity.ts | 43 +++-- src/config/database.config.ts | 13 ++ src/form/dto/create-form.dto.ts | 9 +- src/form/entity/form.entity.ts | 77 ++++----- src/form/form.controller.ts | 1 - src/form/form.module.ts | 22 ++- src/form/form.service.ts | 84 ++++++---- src/formPage/dto/create-formPage.dto.ts | 2 +- src/formPage/entity/formPage.entity.ts | 53 ++---- src/formPage/formPage.controller.ts | 1 - src/formPage/formPage.module.ts | 18 ++- src/formPage/formPage.service.ts | 68 ++++---- src/formSection/entity/formSection.entity.ts | 95 ++++------- src/formSection/formSection.controller.ts | 2 +- src/formSection/formSection.module.ts | 17 +- src/formSection/formSection.service.ts | 71 ++++---- src/main.ts | 1 + src/option/dto/create-option.dto.ts | 5 +- src/option/entity/option.entity.ts | 19 ++- src/option/option.controller.ts | 14 +- src/option/option.module.ts | 6 +- src/option/option.service.ts | 79 ++++++--- src/participant/dto/create-participant.dto.ts | 10 +- src/participant/dto/update-participant.dto.ts | 2 +- src/participant/entity/participant.entity.ts | 40 ++--- src/participant/participant.controller.ts | 25 ++- src/participant/participant.module.ts | 6 +- src/participant/participant.service.ts | 103 ++++++------ .../dto/create-participantGroup.dto.ts | 2 +- .../entity/participantGroup.entity.ts | 39 ++--- .../participantGroup.module.ts | 10 +- .../participantGroup.service.ts | 80 +++++----- src/question/dto/create-question.dto.ts | 18 ++- src/question/entity/question.entity.ts | 66 +++----- src/question/question.module.ts | 12 +- src/question/question.service.ts | 85 ++++------ 52 files changed, 913 insertions(+), 835 deletions(-) create mode 100644 src/config/database.config.ts diff --git a/package-lock.json b/package-lock.json index abf78f5..e0854a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@nestjs/mapped-types": "^2.1.0", "@nestjs/mongoose": "^11.0.3", "@nestjs/platform-express": "^11.1.3", + "@nestjs/typeorm": "^11.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "libphonenumber-js": "^1.12.9", @@ -22,6 +23,7 @@ "mongoose": "^8.16.3", "mongoose-paginate-v2": "^1.9.1", "nestjs-paginate": "^12.5.1", + "pg": "^8.11.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", "uuid": "^11.1.0" @@ -2670,13 +2672,13 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.3.tgz", - "integrity": "sha512-hEDNMlaPiBO72fxxX/CuRQL3MEhKRc/sIYGVoXjrnw6hTxZdezvvM6A95UaLsYknfmcZZa/CdG1SMBZOu9agHQ==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.5.tgz", + "integrity": "sha512-OsoiUBY9Shs5IG3uvDIt9/IDfY5OlvWBESuB/K4Eun8xILw1EK5d5qMfC3d2sIJ+kA3l+kBR1d/RuzH7VprLIg==", "dependencies": { "cors": "2.8.5", "express": "5.1.0", - "multer": "2.0.1", + "multer": "2.0.2", "path-to-regexp": "8.2.0", "tslib": "2.8.1" }, @@ -2841,6 +2843,18 @@ } } }, + "node_modules/@nestjs/typeorm": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", + "integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0" + } + }, "node_modules/@noble/hashes": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", @@ -7158,9 +7172,9 @@ } }, "node_modules/form-data": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", - "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dev": true, "dependencies": { "asynckit": "^0.4.0", @@ -9350,9 +9364,9 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/multer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", - "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz", + "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==", "dependencies": { "append-field": "^1.0.0", "busboy": "^1.6.0", @@ -9834,6 +9848,87 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9989,6 +10084,41 @@ "node": ">= 0.4" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -10820,7 +10950,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "peer": true, "engines": { "node": ">= 10.x" } diff --git a/package.json b/package.json index 660ebe8..f040381 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,8 @@ "@nestjs/mapped-types": "^2.1.0", "@nestjs/mongoose": "^11.0.3", "@nestjs/platform-express": "^11.1.3", + "@nestjs/typeorm": "^11.0.0", + "pg": "^8.11.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", "libphonenumber-js": "^1.12.9", diff --git a/src/_core/dto/base.dto.ts b/src/_core/dto/base.dto.ts index 129c982..51caa8e 100644 --- a/src/_core/dto/base.dto.ts +++ b/src/_core/dto/base.dto.ts @@ -1,18 +1,18 @@ import { IsString, IsEnum, IsOptional, IsUUID, ValidateNested } from 'class-validator'; import { Type } from 'class-transformer'; -import {MetadataDto} from './metadataEntry.dto'; +import { MetadataDto } from './metadataEntry.dto'; export class BaseDto { @IsUUID() - @IsOptional() // id is optional since BaseEntity generates it + @IsOptional() id?: string; @IsEnum(['active', 'inactive']) @IsOptional() - status?: 'active'| 'inactive'; + status?: 'active' | 'inactive'; @ValidateNested() @Type(() => MetadataDto) @IsOptional() - metadata?: MetadataDto; // Optional, defaults to { entries: [] } + metadata?: MetadataDto; } \ No newline at end of file diff --git a/src/_core/dto/metadataEntry.dto.ts b/src/_core/dto/metadataEntry.dto.ts index 1e895e3..48d8881 100644 --- a/src/_core/dto/metadataEntry.dto.ts +++ b/src/_core/dto/metadataEntry.dto.ts @@ -14,4 +14,3 @@ export class MetadataDto { @Type(() => MetadataEntryDto) entries: MetadataEntryDto[]; } - diff --git a/src/_core/entity/_base.entity.ts b/src/_core/entity/_base.entity.ts index 5de08a0..e45888b 100644 --- a/src/_core/entity/_base.entity.ts +++ b/src/_core/entity/_base.entity.ts @@ -1,47 +1,32 @@ -import { Prop, Schema } from '@nestjs/mongoose'; +import { Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, BeforeInsert } from 'typeorm'; +import { Metadata } from './_metadata.entity'; import { v4 as uuidv4 } from 'uuid'; -import { Metadata, MetadataSchema } from './_metadata.entity'; -@Schema({ _id: false, id: false }) // Disable _id and virtual id export abstract class BaseEntity { - @Prop({ - type: String, - default: () => uuidv4(), - required: true, - unique: true, - immutable: true, - }) + @PrimaryGeneratedColumn('uuid') id: string; - @Prop({ - type: Date, - default: () => new Date(), - required: true, - immutable: true, - }) + @CreateDateColumn() createdAt: Date; - @Prop({ - type: Date, - default: () => new Date(), - required: true, - }) + @UpdateDateColumn() updatedAt: Date; - @Prop({ - type: MetadataSchema, - default: () => ({ entries: [] }), - required: true, - }) + @Column(() => Metadata) metadata: Metadata; - @Prop({ - type: String, + @Column({ + type: 'enum', enum: ['active', 'inactive'], default: 'active', - required: true, }) status: 'active' | 'inactive'; -} -export type BaseEntityDocument = BaseEntity & Document; \ No newline at end of file + @BeforeInsert() + generateUuid() { + if (!this.id) { + this.id = uuidv4(); + } + } + +} \ No newline at end of file diff --git a/src/_core/entity/_metadata.entity.ts b/src/_core/entity/_metadata.entity.ts index a6e9b09..4921697 100644 --- a/src/_core/entity/_metadata.entity.ts +++ b/src/_core/entity/_metadata.entity.ts @@ -1,28 +1,17 @@ -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document } from 'mongoose'; -import { v4 as uuidv4 } from 'uuid'; +import { Column, Entity } from 'typeorm'; -@Schema({ _id: false }) // Embedded document +@Entity({ name: 'metadata' }) export class Metadata { - @Prop({ - type: String, - default: () => uuidv4(), - required: true, - immutable: true, + @Column({ + type: 'uuid', + primary: true, + default: () => 'uuid_generate_v4()', }) id: string; - @Prop({ - type: [ - { - key: { type: String, required: true }, - value: { type: String, required: true }, - }, - ], - default: [], + @Column({ + type: 'jsonb', + default: () => "'[]'", }) entries: { key: string; value: string }[]; -} - -export type MetadataDocument = Metadata & Document; -export const MetadataSchema = SchemaFactory.createForClass(Metadata); +} \ No newline at end of file diff --git a/src/answer/answer.controller.ts b/src/answer/answer.controller.ts index 5b0795d..d1d5a4a 100644 --- a/src/answer/answer.controller.ts +++ b/src/answer/answer.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus,} from '@nestjs/common'; +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'; @@ -19,7 +19,6 @@ export class AnswerController { @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)); } @@ -44,4 +43,4 @@ export class AnswerController { async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise { return this.answerService.remove(id); } -} +} \ No newline at end of file diff --git a/src/answer/answer.module.ts b/src/answer/answer.module.ts index 830cc17..a137f0f 100644 --- a/src/answer/answer.module.ts +++ b/src/answer/answer.module.ts @@ -1,16 +1,14 @@ import { Module } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { AnswerService } from './answer.service'; import { AnswerController } from './answer.controller'; -import { Answer, AnswerSchema } from './entity/answer.entity'; +import { Answer } from './entity/answer.entity'; @Module({ imports: [ - MongooseModule.forFeature([ - { name: Answer.name, schema: AnswerSchema }, - ]), + TypeOrmModule.forFeature([Answer]), ], controllers: [AnswerController], providers: [AnswerService], }) -export class AnswerModule {} +export class AnswerModule {} \ No newline at end of file diff --git a/src/answer/answer.service.ts b/src/answer/answer.service.ts index 045606a..baf32ec 100644 --- a/src/answer/answer.service.ts +++ b/src/answer/answer.service.ts @@ -1,25 +1,28 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; -import { Answer, AnswerDocument } from './entity/answer.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Answer } 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, + @InjectRepository(Answer) + private readonly answerRepository: Repository, ) {} 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(); + try { + const answer = this.answerRepository.create({ + ...data, + metadata: data.metadata || { entries: [] }, + status: data.status || 'active', + }); + return await this.answerRepository.save(answer); + } catch (error) { + throw new Error(`Failed to create answer: ${error.message}`); + } } async findAll( @@ -36,46 +39,62 @@ export class AnswerService { nextPage: number | null; prevPage: number | null; }> { - return this.answerModel.paginate( - {}, - { page, limit, lean: true }, - ); + try { + const skip = (page - 1) * limit; + const [docs, totalDocs] = await this.answerRepository.findAndCount({ + skip, + take: limit, + }); + const totalPages = Math.ceil(totalDocs / limit); + return { + docs, + totalDocs, + limit, + page, + totalPages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1, + nextPage: page < totalPages ? page + 1 : null, + prevPage: page > 1 ? page - 1 : null, + }; + } catch (error) { + throw new Error(`Failed to fetch answers: ${error.message}`); + } } 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`); + try { + const answer = await this.answerRepository.findOne({ where: { id } }); + if (!answer) { + throw new NotFoundException(`Answer with ID "${id}" not found`); + } + return answer; + } catch (error) { + throw new Error(`Failed to find answer: ${error.message}`); } - 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`); + async update(id: string, data: UpdateAnswerDto): Promise { + try { + await this.answerRepository.update({ id }, { ...data, updatedAt: new Date() }); + const updatedAnswer = await this.answerRepository.findOne({ where: { id } }); + if (!updatedAnswer) { + throw new NotFoundException(`Answer with ID "${id}" not found`); + } + return updatedAnswer; + } catch (error) { + throw new Error(`Failed to update answer: ${error.message}`); } - 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`); + try { + const result = await this.answerRepository.delete({ id }); + if (result.affected === 0) { + throw new NotFoundException(`Answer with ID "${id}" not found`); + } + } catch (error) { + throw new Error(`Failed to delete answer: ${error.message}`); } } -} +} \ No newline at end of file diff --git a/src/answer/dto/create-answer.dto.ts b/src/answer/dto/create-answer.dto.ts index d2513b2..539c44c 100644 --- a/src/answer/dto/create-answer.dto.ts +++ b/src/answer/dto/create-answer.dto.ts @@ -1,7 +1,7 @@ -import { IsString, IsUUID} from 'class-validator'; +import { IsString, IsUUID } from 'class-validator'; import { BaseDto } from '../../_core/dto/base.dto'; -export class CreateAnswerDto extends BaseDto{ +export class CreateAnswerDto extends BaseDto { @IsUUID() participantId: string; @@ -10,4 +10,4 @@ export class CreateAnswerDto extends BaseDto{ @IsString() value: string; -} +} \ No newline at end of file diff --git a/src/answer/entity/answer.entity.ts b/src/answer/entity/answer.entity.ts index b735b40..a09a292 100644 --- a/src/answer/entity/answer.entity.ts +++ b/src/answer/entity/answer.entity.ts @@ -1,44 +1,22 @@ -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document } from 'mongoose'; +import { Column, Entity, ManyToOne } from 'typeorm'; import { BaseEntity } from 'src/_core/entity/_base.entity'; -import * as mongoosePaginate from 'mongoose-paginate-v2'; -import { v4 as uuidv4 } from 'uuid'; +import { Question } from '../../question/entity/question.entity'; -@Schema({ _id: false, id: false }) // Disable _id and virtual id +@Entity({ name: 'answers' }) export class Answer extends BaseEntity { - @Prop({ - type: String, - default: () => uuidv4(), - required: true, - immutable: true, + @Column({ + type: 'uuid', }) participantId: string; - @Prop({ - type: String, - default: () => uuidv4(), - required: true, - immutable: true, + @Column({ + type: 'uuid', }) - questionId: string; // form can be identified based on question + questionId: string; - @Prop({ - type: String, - required: true, - }) - value: string; //value can be mapped to option based on question type + @Column() + value: string; - -} - -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; - }, -}); + @ManyToOne(() => Question, question => question.answers) + question: Question; +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index e6f266a..eb69048 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,6 +1,5 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { MongooseModule } from '@nestjs/mongoose'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ParticipantModule } from './participant/participant.module'; @@ -14,13 +13,15 @@ import { FormSectionModule } from './formSection/formSection.module'; import { OptionModule } from './option/option.module'; import { ParticipanGrouptModule } from './participantGroup/participantGroup.module'; import { RealmModule } from './realm/realm.module'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { typeOrmConfig } from './config/database.config'; @Module({ imports: [ + TypeOrmModule.forRoot(typeOrmConfig), ConfigModule.forRoot({ isGlobal: true, // Makes ConfigModule available globally }), - MongooseModule.forRoot(process.env.MONGODB_URI || 'mongodb://localhost:27017/db'), ParticipantModule, QuestionModule, AnswerModule, diff --git a/src/attachment/attachment.controller.ts b/src/attachment/attachment.controller.ts index ad93f2a..61ebea4 100644 --- a/src/attachment/attachment.controller.ts +++ b/src/attachment/attachment.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus,} from '@nestjs/common'; +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'; @@ -19,7 +19,6 @@ export class AttachmentController { @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)); } @@ -44,4 +43,4 @@ export class AttachmentController { async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise { return this.attachmentService.remove(id); } -} +} \ No newline at end of file diff --git a/src/attachment/attachment.module.ts b/src/attachment/attachment.module.ts index 7b0c053..85accf8 100644 --- a/src/attachment/attachment.module.ts +++ b/src/attachment/attachment.module.ts @@ -1,16 +1,14 @@ import { Module } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; -import { Attachment, AttachmentSchema } from './entity/attachment.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Attachment } from './entity/attachment.entity'; import { AttachmentController } from './attachment.controller'; import { AttachmentService } from './attachment.service'; @Module({ imports: [ - MongooseModule.forFeature([ - { name: Attachment.name, schema: AttachmentSchema }, - ]), + TypeOrmModule.forFeature([Attachment]), ], controllers: [AttachmentController], providers: [AttachmentService], }) -export class AttachmentModule {} +export class AttachmentModule {} \ No newline at end of file diff --git a/src/attachment/attachment.service.ts b/src/attachment/attachment.service.ts index 30188a0..3319fed 100644 --- a/src/attachment/attachment.service.ts +++ b/src/attachment/attachment.service.ts @@ -1,26 +1,28 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; -import { Attachment, AttachmentDocument } from './entity/attachment.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Attachment } from './entity/attachment.entity'; import { CreateAttachmentDto } from './dto/create_attachment.dto'; import { UpdateAttachmentDto } from './dto/update_attachment.dto'; -import { PaginateModel } from '../participant/participant.service'; - @Injectable() export class AttachmentService { constructor( - @InjectModel(Attachment.name) - private readonly attachmentModel: PaginateModel, + @InjectRepository(Attachment) + private readonly attachmentRepository: Repository, ) {} 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(); + try { + const attachment = this.attachmentRepository.create({ + ...data, + metadata: data.metadata || { entries: [] }, + status: data.status || 'active', + }); + return await this.attachmentRepository.save(attachment); + } catch (error) { + throw new Error(`Failed to create attachment: ${error.message}`); + } } async findAll( @@ -37,47 +39,62 @@ export class AttachmentService { nextPage: number | null; prevPage: number | null; }> { - // Selects all fields from the Attachment entity for the response - return this.attachmentModel.paginate( - {}, - { page, limit, lean: true }, - ); + try { + const skip = (page - 1) * limit; + const [docs, totalDocs] = await this.attachmentRepository.findAndCount({ + skip, + take: limit, + }); + const totalPages = Math.ceil(totalDocs / limit); + return { + docs, + totalDocs, + limit, + page, + totalPages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1, + nextPage: page < totalPages ? page + 1 : null, + prevPage: page > 1 ? page - 1 : null, + }; + } catch (error) { + throw new Error(`Failed to fetch attachments: ${error.message}`); + } } async findById(id: string): Promise { - const attachment = await this.attachmentModel - .findOne({ id }) - .lean() - .exec(); - if (!attachment) { - throw new NotFoundException(`Attachment with ID "${id}" not found`); + try { + const attachment = await this.attachmentRepository.findOne({ where: { id } }); + if (!attachment) { + throw new NotFoundException(`Attachment with ID "${id}" not found`); + } + return attachment; + } catch (error) { + throw new Error(`Failed to find attachment: ${error.message}`); } - return attachment; } - async update( - id: string, - data: UpdateAttachmentDto, - ): Promise { - const updatedAttachment = await this.attachmentModel - .findOneAndUpdate( - { id }, - { $set: { ...data, updatedAt: new Date() } }, - { new: true }, - ) - .lean() - .exec(); - - if (!updatedAttachment) { - throw new NotFoundException(`Attachment with ID "${id}" not found`); + async update(id: string, data: UpdateAttachmentDto): Promise { + try { + await this.attachmentRepository.update({ id }, { ...data, updatedAt: new Date() }); + const updatedAttachment = await this.attachmentRepository.findOne({ where: { id } }); + if (!updatedAttachment) { + throw new NotFoundException(`Attachment with ID "${id}" not found`); + } + return updatedAttachment; + } catch (error) { + throw new Error(`Failed to update attachment: ${error.message}`); } - 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`); + try { + const result = await this.attachmentRepository.delete({ id }); + if (result.affected === 0) { + throw new NotFoundException(`Attachment with ID "${id}" not found`); + } + } catch (error) { + throw new Error(`Failed to delete attachment: ${error.message}`); } } -} +} \ No newline at end of file diff --git a/src/attachment/dto/create_attachment.dto.ts b/src/attachment/dto/create_attachment.dto.ts index 23f7bb1..dec3faf 100644 --- a/src/attachment/dto/create_attachment.dto.ts +++ b/src/attachment/dto/create_attachment.dto.ts @@ -1,17 +1,16 @@ -// DTO for embedded Attachment -import { isEnum, IsEnum, IsOptional, IsString, IsUrl, IsUUID, ValidateNested } from 'class-validator'; +import { 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{ +export class CreateAttachmentDto extends BaseDto { @IsString() displayName: string; @IsString() filePath: string; - @IsEnum(['png','pdf','csv','jpg']) - fileType: 'png'|'pdf'|'csv'|'jpg'; + @IsEnum(['png', 'pdf', 'csv', 'jpg']) + fileType: 'png' | 'pdf' | 'csv' | 'jpg'; @IsUrl() storageUrl: string; diff --git a/src/attachment/entity/attachment.entity.ts b/src/attachment/entity/attachment.entity.ts index 2550d45..55d98a8 100644 --- a/src/attachment/entity/attachment.entity.ts +++ b/src/attachment/entity/attachment.entity.ts @@ -1,40 +1,33 @@ -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document } from 'mongoose'; +import { Column, Entity, ManyToOne } from 'typeorm'; import { BaseEntity } from 'src/_core/entity/_base.entity'; +import { Question } from 'src/question/entity/question.entity'; +import { FormSection } from '../../formSection/entity/formSection.entity'; -@Schema({ _id: false, id: false }) // Embedded document, inheriting from BaseEntity +@Entity({ name: 'attachments' }) export class Attachment extends BaseEntity { - @Prop({ - type: String, - required: true, - }) + @Column() displayName: string; - @Prop({ - type: String, - required: true, - }) + @Column() filePath: string; - @Prop({ - type: String, + @Column({ + type: 'enum', enum: ['png', 'pdf', 'csv', 'jpg'], - required: true, }) fileType: 'png' | 'pdf' | 'csv' | 'jpg'; - @Prop({ - type: String, - required: true, - }) + @Column() storageUrl: string; - @Prop({ - type: String, - required: true, + @Column({ + type: 'uuid', }) - ownerId: string; // UUID of the user who uploaded it -} + ownerId: string; -export type AttachmentDocument = Attachment & Document; -export const AttachmentSchema = SchemaFactory.createForClass(Attachment); + @ManyToOne(() => Question, (question) => question.attachments) + question: Question; + + @ManyToOne(() => FormSection, formSection => formSection.attachments) + formSection: FormSection; +} \ No newline at end of file diff --git a/src/config/database.config.ts b/src/config/database.config.ts new file mode 100644 index 0000000..0449181 --- /dev/null +++ b/src/config/database.config.ts @@ -0,0 +1,13 @@ +import { TypeOrmModuleOptions } from '@nestjs/typeorm'; + +export const typeOrmConfig: TypeOrmModuleOptions = { + type: 'postgres', + host: 'localhost', + port: 5432, + username: 'your_username', + password: 'your_password', + database: 'your_database', + entities: [__dirname + '/**/*.entity{.ts,.js}'], + synchronize: true, + extensions: ['uuid-ossp'], +}; \ No newline at end of file diff --git a/src/form/dto/create-form.dto.ts b/src/form/dto/create-form.dto.ts index 0a83d99..def1312 100644 --- a/src/form/dto/create-form.dto.ts +++ b/src/form/dto/create-form.dto.ts @@ -1,10 +1,9 @@ -import { IsString, IsOptional, IsUUID, ValidateNested, IsArray, IsEnum, IsDateString } from 'class-validator'; +import { IsString, IsOptional, ValidateNested, IsArray } from 'class-validator'; import { Type } from 'class-transformer'; import { BaseDto } from '../../_core/dto/base.dto'; -import { CreateAttachmentDto } from '../../attachment/dto/create_attachment.dto'; -import { CreateQuestionDto } from '../../question/dto/create-question.dto'; import { CreateFormPageDto } from '../../formPage/dto/create-formPage.dto'; import { CreateParticipantDto } from '../../participant/dto/create-participant.dto'; +import { CreateAttachmentDto } from '../../attachment/dto/create_attachment.dto'; export class CreateFormDto extends BaseDto { @IsString() @@ -17,7 +16,7 @@ export class CreateFormDto extends BaseDto { @ValidateNested({ each: true }) @Type(() => CreateAttachmentDto) @IsOptional() - attachments?: CreateAttachmentDto[]; + attachments?: CreateAttachmentDto[]; // Keep if attachments are still part of the DTO @IsArray() @ValidateNested({ each: true }) @@ -28,4 +27,4 @@ export class CreateFormDto extends BaseDto { @ValidateNested({ each: true }) @Type(() => CreateParticipantDto) participants: CreateParticipantDto[]; -} \ No newline at end of file +} diff --git a/src/form/entity/form.entity.ts b/src/form/entity/form.entity.ts index 25ff6da..8a1fb30 100644 --- a/src/form/entity/form.entity.ts +++ b/src/form/entity/form.entity.ts @@ -1,58 +1,39 @@ -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 { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity'; -import { Option, OptionSchema } from '../../option/entity/option.entity'; -import { Answer, AnswerSchema } from '../../answer/entity/answer.entity'; -import { Question, QuestionSchema } from '../../question/entity/question.entity'; -import { Participant, ParticipantSchema } from '../../participant/entity/participant.entity'; -import { FormPage, FormPageSchema } from '../../formPage/entity/formPage.entity'; +import { Entity, Column, OneToMany } from 'typeorm'; +import { BaseEntity } from '../../_core/entity/_base.entity'; +import { FormPage } from '../../formPage/entity/formPage.entity'; +import { Participant } from '../../participant/entity/participant.entity'; // Assuming Participant entity exists -@Schema({ _id: false, id: false }) // Disable _id and virtual id +@Entity() export class Form extends BaseEntity { - @Prop({ - type: String, - required: true, - }) + @Column() title: string; - @Prop({ - type: String, - required: true, - }) + @Column() description: string; - @Prop({ - type: [AttachmentSchema], - default: [], - }) - attachments: Attachment[]; - - @Prop({ - type: [FormPageSchema], - default: [], - }) + // One-to-Many relationship with FormPage + @OneToMany(() => FormPage, formPage => formPage.form, { cascade: true, eager: true }) pages: FormPage[]; - @Prop({ - type: [ParticipantSchema], - default: [], - }) + // One-to-Many relationship with Participant + @OneToMany(() => Participant, participant => participant.form, { cascade: true, eager: true }) participants: Participant[]; + + // Attachments are typically stored in a separate table and linked via a many-to-many or one-to-many + // For simplicity, if attachments are embedded, they would be handled like DisplayCondition in FormSection. + // If they are separate entities, they would need a relationship defined here. + // Assuming attachments are now handled as a separate entity with a relationship if needed. + // If attachments were previously embedded, you'd need to define a class for them and use @Column(() => AttachmentClass) + // For now, removing the direct 'attachments' column from Form as it was an array of AttachmentSchema (Mongoose). + // If you need attachments directly on Form, please provide the Attachment entity structure. + + + + + // @Prop({ + // type: [AttachmentSchema], + // default: [], + // }) + // attachments: Attachment[]; + } - -export type FormDocument = Form & Document; -export const FormSchema = SchemaFactory.createForClass(Form); -FormSchema.plugin(mongoosePaginate); - -// Transform the output to remove the internal '_id' -FormSchema.set('toJSON', { - transform: (doc: FormDocument, ret: Form & { _id?: any }) => { - delete ret._id; - return ret; - }, -}); - - - diff --git a/src/form/form.controller.ts b/src/form/form.controller.ts index d31fe07..abe6af9 100644 --- a/src/form/form.controller.ts +++ b/src/form/form.controller.ts @@ -19,7 +19,6 @@ export class FormController { @Query('page') page: string = '1', @Query('limit') limit: string = '10', ) { - // The service returns the full pagination object return this.formService.findAll(parseInt(page, 10), parseInt(limit, 10)); } diff --git a/src/form/form.module.ts b/src/form/form.module.ts index bfd3395..ad7cbe2 100644 --- a/src/form/form.module.ts +++ b/src/form/form.module.ts @@ -1,13 +1,27 @@ import { Module } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { FormService } from './form.service'; import { FormController } from './form.controller'; -import { Form, FormSchema } from './entity/form.entity'; +import { Form } from './entity/form.entity'; +import { FormPage } from '../formPage/entity/formPage.entity'; +import { FormSection } from '../formSection/entity/formSection.entity'; +import { Attachment } from '../attachment/entity/attachment.entity'; +import { Question } from '../question/entity/question.entity'; +import { Answer } from '../answer/entity/answer.entity'; +import { Option } from '../option/entity/option.entity'; +import { Participant } from '../participant/entity/participant.entity'; // Assuming Participant entity exists @Module({ imports: [ - MongooseModule.forFeature([ - { name: Form.name, schema: FormSchema }, + TypeOrmModule.forFeature([ + Form, + FormPage, + FormSection, + Attachment, + Question, + Answer, + Option, + Participant, // Include Participant entity ]), ], controllers: [FormController], diff --git a/src/form/form.service.ts b/src/form/form.service.ts index 35f572f..a229700 100644 --- a/src/form/form.service.ts +++ b/src/form/form.service.ts @@ -1,25 +1,22 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; -import { Form, FormDocument } from './entity/form.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Form } from './entity/form.entity'; import { CreateFormDto } from './dto/create-form.dto'; import { UpdateFormDto } from './dto/update-form.dto'; -import { PaginateModel } from '../participant/participant.service'; @Injectable() export class FormService { constructor( - @InjectModel(Form.name) - private readonly formModel: PaginateModel, + @InjectRepository(Form) + private readonly formRepo: Repository
, ) {} async create(data: CreateFormDto): Promise { - const form = new this.formModel({ + const form = this.formRepo.create({ ...data, - id: data.id || undefined, // Let BaseEntity generate UUID if not provided - metadata: data.metadata || { entries: [] }, }); - return form.save(); + return await this.formRepo.save(form); } async findAll( @@ -36,18 +33,47 @@ export class FormService { nextPage: number | null; prevPage: number | null; }> { - // Selects all fields from the Form entity for the response - return this.formModel.paginate( - {}, - { page, limit, lean: true }, - ); + const [docs, totalDocs] = await this.formRepo.findAndCount({ + skip: (page - 1) * limit, + take: limit, + relations: [ + 'pages', + 'pages.formSections', + 'pages.formSections.attachments', + 'pages.formSections.questions', + 'pages.formSections.questions.options', + 'pages.formSections.questions.answers', + 'participants', // Load participants + ], // Load nested relations + }); + + const totalPages = Math.ceil(totalDocs / limit); + return { + docs, + totalDocs, + limit, + page, + totalPages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1, + nextPage: page < totalPages ? page + 1 : null, + prevPage: page > 1 ? page - 1 : null, + }; } async findById(id: string): Promise { - const form = await this.formModel - .findOne({ id }) - .lean() - .exec(); + const form = await this.formRepo.findOne({ + where: { id }, + relations: [ + 'pages', + 'pages.formSections', + 'pages.formSections.attachments', + 'pages.formSections.questions', + 'pages.formSections.questions.options', + 'pages.formSections.questions.answers', + 'participants', + ], // Load nested relations + }); if (!form) { throw new NotFoundException(`Form with ID "${id}" not found`); } @@ -58,24 +84,20 @@ export class FormService { id: string, data: UpdateFormDto, ): Promise { - const updatedForm = await this.formModel - .findOneAndUpdate( - { id }, - { $set: { ...data, updatedAt: new Date() } }, - { new: true }, - ) - .lean() - .exec(); + const result = await this.formRepo.update( + { id }, + { ...data, updatedAt: new Date() }, + ); - if (!updatedForm) { + if (result.affected === 0) { throw new NotFoundException(`Form with ID "${id}" not found`); } - return updatedForm; + return await this.findById(id); } async remove(id: string): Promise { - const result = await this.formModel.deleteOne({ id }).exec(); - if (result.deletedCount === 0) { + const result = await this.formRepo.delete({ id }); + if (result.affected === 0) { throw new NotFoundException(`Form with ID "${id}" not found`); } } diff --git a/src/formPage/dto/create-formPage.dto.ts b/src/formPage/dto/create-formPage.dto.ts index 84351ab..6fe8bf2 100644 --- a/src/formPage/dto/create-formPage.dto.ts +++ b/src/formPage/dto/create-formPage.dto.ts @@ -14,4 +14,4 @@ export class CreateFormPageDto extends BaseDto { @ValidateNested({ each: true }) @Type(() => CreateFormSectionDto) formSections: CreateFormSectionDto[]; -} \ No newline at end of file +} diff --git a/src/formPage/entity/formPage.entity.ts b/src/formPage/entity/formPage.entity.ts index d2c78db..cb22a7a 100644 --- a/src/formPage/entity/formPage.entity.ts +++ b/src/formPage/entity/formPage.entity.ts @@ -1,45 +1,22 @@ -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 { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity'; -import { Option, OptionSchema } from '../../option/entity/option.entity'; -import { Answer, AnswerSchema } from '../../answer/entity/answer.entity'; -import { Question, QuestionSchema } from '../../question/entity/question.entity'; -import { FormSection, FormSectionSchema } from '../../formSection/entity/formSection.entity'; +import { Entity, Column, OneToMany, ManyToOne } from 'typeorm'; +import { BaseEntity } from '../../_core/entity/_base.entity'; +import { FormSection } from '../../formSection/entity/formSection.entity'; +import { Form } from '../../form/entity/form.entity'; -@Schema({ _id: false, id: false }) // Disable _id and virtual id +@Entity() export class FormPage extends BaseEntity { - @Prop({ - type: String, - required: true, - }) + @Column() title: string; - @Prop({ - type: String, - required: true, - }) + @Column() description: string; - @Prop({ - type: [FormSectionSchema], - default: [], - }) - attachments: FormSection[]; + // One-to-Many relationship with FormSection + @OneToMany(() => FormSection, formSection => formSection.formPage, { cascade: true, eager: true }) + formSections: FormSection[]; + + // Many-to-One relationship with Form + @ManyToOne(() => Form, form => form.pages) // Ensure 'pages' matches the property name in Form entity + form: Form; + } - -export type FormPageDocument = FormPage & Document; -export const FormPageSchema = SchemaFactory.createForClass(FormPage); -FormPageSchema.plugin(mongoosePaginate); - -// Transform the output to remove the internal '_id' -FormPageSchema.set('toJSON', { - transform: (doc: FormPageDocument, ret: FormPage & { _id?: any }) => { - delete ret._id; - return ret; - }, -}); - - - diff --git a/src/formPage/formPage.controller.ts b/src/formPage/formPage.controller.ts index 3e29fe4..740f7ff 100644 --- a/src/formPage/formPage.controller.ts +++ b/src/formPage/formPage.controller.ts @@ -19,7 +19,6 @@ export class FormPageController { @Query('page') page: string = '1', @Query('limit') limit: string = '10', ) { - // The service returns the full pagination object return this.formPageService.findAll(parseInt(page, 10), parseInt(limit, 10)); } diff --git a/src/formPage/formPage.module.ts b/src/formPage/formPage.module.ts index 9cec8b2..2159634 100644 --- a/src/formPage/formPage.module.ts +++ b/src/formPage/formPage.module.ts @@ -1,13 +1,23 @@ import { Module } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { FormPageService } from './formPage.service'; import { FormPageController } from './formPage.controller'; -import { FormPage, FormPageSchema } from './entity/formPage.entity'; +import { FormPage } from './entity/formPage.entity'; +import { FormSection } from '../formSection/entity/formSection.entity'; +import { Attachment } from '../attachment/entity/attachment.entity'; +import { Question } from '../question/entity/question.entity'; +import { Answer } from '../answer/entity/answer.entity'; +import { Option } from '../option/entity/option.entity'; @Module({ imports: [ - MongooseModule.forFeature([ - { name: FormPage.name, schema: FormPageSchema }, + TypeOrmModule.forFeature([ + FormPage, + FormSection, + Attachment, + Question, + Answer, + Option, ]), ], controllers: [FormPageController], diff --git a/src/formPage/formPage.service.ts b/src/formPage/formPage.service.ts index b97c490..852c852 100644 --- a/src/formPage/formPage.service.ts +++ b/src/formPage/formPage.service.ts @@ -1,25 +1,22 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; -import { FormPage, FormPageDocument } from './entity/formPage.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { FormPage } from './entity/formPage.entity'; import { CreateFormPageDto } from './dto/create-formPage.dto'; import { UpdateFormPageDto } from './dto/update-formPage.dto'; -import { PaginateModel } from '../participant/participant.service'; @Injectable() export class FormPageService { constructor( - @InjectModel(FormPage.name) - private readonly formPageModel: PaginateModel, + @InjectRepository(FormPage) + private readonly formPageRepo: Repository, ) {} async create(data: CreateFormPageDto): Promise { - const formPage = new this.formPageModel({ + const formPage = this.formPageRepo.create({ ...data, - id: data.id || undefined, // Let BaseEntity generate UUID if not provided - metadata: data.metadata || { entries: [] }, }); - return formPage.save(); + return await this.formPageRepo.save(formPage); } async findAll( @@ -36,18 +33,31 @@ export class FormPageService { nextPage: number | null; prevPage: number | null; }> { - // Selects all fields from the FormPage entity for the response - return this.formPageModel.paginate( - {}, - { page, limit, lean: true }, - ); + const [docs, totalDocs] = await this.formPageRepo.findAndCount({ + skip: (page - 1) * limit, + take: limit, + relations: ['formSections', 'formSections.attachments', 'formSections.questions', 'formSections.questions.options', 'formSections.questions.answers'], // Load related data + }); + + const totalPages = Math.ceil(totalDocs / limit); + return { + docs, + totalDocs, + limit, + page, + totalPages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1, + nextPage: page < totalPages ? page + 1 : null, + prevPage: page > 1 ? page - 1 : null, + }; } async findById(id: string): Promise { - const formPage = await this.formPageModel - .findOne({ id }) - .lean() - .exec(); + const formPage = await this.formPageRepo.findOne({ + where: { id }, + relations: ['formSections', 'formSections.attachments', 'formSections.questions', 'formSections.questions.options', 'formSections.questions.answers'], // Load related data + }); if (!formPage) { throw new NotFoundException(`FormPage with ID "${id}" not found`); } @@ -58,24 +68,20 @@ export class FormPageService { id: string, data: UpdateFormPageDto, ): Promise { - const updatedFormPage = await this.formPageModel - .findOneAndUpdate( - { id }, - { $set: { ...data, updatedAt: new Date() } }, - { new: true }, - ) - .lean() - .exec(); + const result = await this.formPageRepo.update( + { id }, + { ...data, updatedAt: new Date() }, + ); - if (!updatedFormPage) { + if (result.affected === 0) { throw new NotFoundException(`FormPage with ID "${id}" not found`); } - return updatedFormPage; + return await this.findById(id); } async remove(id: string): Promise { - const result = await this.formPageModel.deleteOne({ id }).exec(); - if (result.deletedCount === 0) { + const result = await this.formPageRepo.delete({ id }); + if (result.affected === 0) { throw new NotFoundException(`FormPage with ID "${id}" not found`); } } diff --git a/src/formSection/entity/formSection.entity.ts b/src/formSection/entity/formSection.entity.ts index 874a5fe..d965668 100644 --- a/src/formSection/entity/formSection.entity.ts +++ b/src/formSection/entity/formSection.entity.ts @@ -1,85 +1,46 @@ -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 { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity'; -import { Option, OptionSchema } from '../../option/entity/option.entity'; -import { Answer, AnswerSchema } from '../../answer/entity/answer.entity'; -import { Question, QuestionSchema } from '../../question/entity/question.entity'; +// src/formSection/entity/formSection.entity.ts +import { Entity, Column, OneToMany, ManyToOne } from 'typeorm'; // Removed Embeddable import +import { BaseEntity } from '../../_core/entity/_base.entity'; +import { Attachment } from '../../attachment/entity/attachment.entity'; +import { Question } from '../../question/entity/question.entity'; +import { FormPage } from 'src/formPage/entity/formPage.entity'; -// Sub-schema for DisplayContition -@Schema({ _id: false, id: false }) +// DisplayCondition is now a regular class, embedded using @Column(() => DisplayCondition) export class DisplayCondition { - @Prop({ - type: [AnswerSchema], - required: true, - }) - answer: Answer; + @Column() + answer: string; // Assuming this is an ID reference to an Answer entity - @Prop({ - type: [QuestionSchema], - required: true, - }) - question: Question; + @Column() + question: string; // Assuming this is an ID reference to a Question entity - @Prop({ - type: String, - enum: ['equal','contains','not_equal','not_contains'], - required: true, + @Column({ + type: 'enum', + enum: ['equal', 'contains', 'not_equal', 'not_contains'], }) - relation: 'equal'|'contains'|'not_equal'|'not_contains'; + relation: 'equal' | 'contains' | 'not_equal' | 'not_contains'; } -export const DisplayConditionSchema = SchemaFactory.createForClass(DisplayCondition); - - -//\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\ - -@Schema({ _id: false, id: false }) // Disable _id and virtual id +@Entity() export class FormSection extends BaseEntity { - @Prop({ - type: String, - required: true, - }) + @Column() title: string; - @Prop({ - type: String, - required: true, - }) + @Column() description: string; - @Prop({ - type: [AttachmentSchema], - default: [], - }) + // One-to-Many relationship with Attachment + @OneToMany(() => Attachment, attachment => attachment.formSection, { cascade: true, eager: true }) attachments: Attachment[]; - @Prop({ - type: [DisplayConditionSchema], - default: [], - }) + // Using @Column(() => DisplayCondition) for embedded array of objects + @Column(() => DisplayCondition) displayCondition: DisplayCondition[]; - @Prop({ - type: [QuestionSchema], - default: [], - }) + // One-to-Many relationship with Question + @OneToMany(() => Question, question => question.formSection, { cascade: true, eager: true }) questions: Question[]; -} - -export type FormSectionDocument = FormSection & Document; -export const FormSectionSchema = SchemaFactory.createForClass(FormSection); -FormSectionSchema.plugin(mongoosePaginate); - -// Transform the output to remove the internal '_id' -FormSectionSchema.set('toJSON', { - transform: (doc: FormSectionDocument, ret: FormSection & { _id?: any }) => { - delete ret._id; - return ret; - }, -}); - - - + // Many-to-One relationship with FormPage + @ManyToOne(() => FormPage, formPage => formPage.formSections) + formPage: FormPage; +} \ No newline at end of file diff --git a/src/formSection/formSection.controller.ts b/src/formSection/formSection.controller.ts index 93cc7a3..971b8c2 100644 --- a/src/formSection/formSection.controller.ts +++ b/src/formSection/formSection.controller.ts @@ -44,4 +44,4 @@ export class FormSectionController { async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise { return this.formSectionService.remove(id); } -} +} \ No newline at end of file diff --git a/src/formSection/formSection.module.ts b/src/formSection/formSection.module.ts index 630fce2..8dde47c 100644 --- a/src/formSection/formSection.module.ts +++ b/src/formSection/formSection.module.ts @@ -1,13 +1,22 @@ +// src/formSection/formSection.module.ts import { Module } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { FormSectionService } from './formSection.service'; import { FormSectionController } from './formSection.controller'; -import { FormSection, FormSectionSchema } from './entity/formSection.entity'; +import { FormSection } from './entity/formSection.entity'; +import { Attachment } from '../attachment/entity/attachment.entity'; +import { Question } from '../question/entity/question.entity'; +import { Answer } from '../answer/entity/answer.entity'; +import { Option } from '../option/entity/option.entity'; @Module({ imports: [ - MongooseModule.forFeature([ - { name: FormSection.name, schema: FormSectionSchema }, + TypeOrmModule.forFeature([ + FormSection, + Attachment, + Question, + Answer, + Option, ]), ], controllers: [FormSectionController], diff --git a/src/formSection/formSection.service.ts b/src/formSection/formSection.service.ts index 97e5e3a..3091fce 100644 --- a/src/formSection/formSection.service.ts +++ b/src/formSection/formSection.service.ts @@ -1,25 +1,22 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; -import { FormSection, FormSectionDocument } from './entity/formSection.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { FormSection } from './entity/formSection.entity'; import { CreateFormSectionDto } from './dto/create-formSection.dto'; import { UpdateFormSectionDto } from './dto/update-formSection.dto'; -import { PaginateModel } from '../participant/participant.service'; @Injectable() export class FormSectionService { constructor( - @InjectModel(FormSection.name) - private readonly formSectionModel: PaginateModel, + @InjectRepository(FormSection) + private readonly formSectionRepo: Repository, ) {} async create(data: CreateFormSectionDto): Promise { - const formSection = new this.formSectionModel({ + const formSection = this.formSectionRepo.create({ ...data, - id: data.id || undefined, // Let BaseEntity generate UUID if not provided - metadata: data.metadata || { entries: [] }, }); - return formSection.save(); + return await this.formSectionRepo.save(formSection); } async findAll( @@ -36,18 +33,31 @@ export class FormSectionService { nextPage: number | null; prevPage: number | null; }> { - // Selects all fields from the FormSection entity for the response - return this.formSectionModel.paginate( - {}, - { page, limit, lean: true }, - ); + const [docs, totalDocs] = await this.formSectionRepo.findAndCount({ + skip: (page - 1) * limit, + take: limit, + relations: ['attachments', 'questions', 'questions.options', 'questions.answers'], // Load related data + }); + + const totalPages = Math.ceil(totalDocs / limit); + return { + docs, + totalDocs, + limit, + page, + totalPages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1, + nextPage: page < totalPages ? page + 1 : null, + prevPage: page > 1 ? page - 1 : null, + }; } async findById(id: string): Promise { - const formSection = await this.formSectionModel - .findOne({ id }) - .lean() - .exec(); + const formSection = await this.formSectionRepo.findOne({ + where: { id }, + relations: ['attachments', 'questions', 'questions.options', 'questions.answers'], // Load related data + }); if (!formSection) { throw new NotFoundException(`FormSection with ID "${id}" not found`); } @@ -58,24 +68,23 @@ export class FormSectionService { id: string, data: UpdateFormSectionDto, ): Promise { - const updatedFormSection = await this.formSectionModel - .findOneAndUpdate( - { id }, - { $set: { ...data, updatedAt: new Date() } }, - { new: true }, - ) - .lean() - .exec(); + // TypeORM's update method returns UpdateResult, not the entity itself. + // We update and then fetch the updated entity. + const result = await this.formSectionRepo.update( + { id }, + { ...data, updatedAt: new Date() }, + ); - if (!updatedFormSection) { + if (result.affected === 0) { throw new NotFoundException(`FormSection with ID "${id}" not found`); } - return updatedFormSection; + // Fetch and return the updated entity to ensure all fields are fresh. + return await this.findById(id); } async remove(id: string): Promise { - const result = await this.formSectionModel.deleteOne({ id }).exec(); - if (result.deletedCount === 0) { + const result = await this.formSectionRepo.delete({ id }); + if (result.affected === 0) { throw new NotFoundException(`FormSection with ID "${id}" not found`); } } diff --git a/src/main.ts b/src/main.ts index f76bc8d..d6a0e76 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + app.enableCors({ origin: 'http://localhost:3001' }); await app.listen(process.env.PORT ?? 3000); } bootstrap(); diff --git a/src/option/dto/create-option.dto.ts b/src/option/dto/create-option.dto.ts index 671c3e5..e0852f7 100644 --- a/src/option/dto/create-option.dto.ts +++ b/src/option/dto/create-option.dto.ts @@ -1,8 +1,7 @@ -import { IsString, IsOptional, ValidateNested, IsUUID, IsEnum } from 'class-validator'; +import { IsString } from 'class-validator'; import { BaseDto } from '../../_core/dto/base.dto'; -// DTO for embedded Option -export class CreateOptionDto extends BaseDto{ +export class CreateOptionDto extends BaseDto { @IsString() text: string; } \ No newline at end of file diff --git a/src/option/entity/option.entity.ts b/src/option/entity/option.entity.ts index 07481e4..636df36 100644 --- a/src/option/entity/option.entity.ts +++ b/src/option/entity/option.entity.ts @@ -1,15 +1,14 @@ -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document } from 'mongoose'; +import { Column, Entity, ManyToOne } from 'typeorm'; import { BaseEntity } from 'src/_core/entity/_base.entity'; +import { Question } from 'src/question/entity/question.entity'; -@Schema({ _id: false, id: false }) // Embedded document, inheriting from BaseEntity +@Entity({ name: 'options' }) export class Option extends BaseEntity { - @Prop({ - type: String, - required: true, - }) + @Column() text: string; -} -export type OptionDocument = Option & Document; -export const OptionSchema = SchemaFactory.createForClass(Option); + @ManyToOne(() => Question, (question) => question.options) + question: Question; + + +} \ No newline at end of file diff --git a/src/option/option.controller.ts b/src/option/option.controller.ts index 3b8120d..7dfea06 100644 --- a/src/option/option.controller.ts +++ b/src/option/option.controller.ts @@ -1,17 +1,15 @@ -import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query } from '@nestjs/common'; -import { ParseUUIDPipe } from '@nestjs/common'; +import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus } from '@nestjs/common'; import { OptionService } from './option.service'; import { CreateOptionDto } from './dto/create-option.dto'; import { UpdateOptionDto } from './dto/update-option.dto'; import { Option } from './entity/option.entity'; -import { Paginate, Paginated, PaginateQuery } from 'nestjs-paginate'; @Controller('options') export class OptionController { constructor(private readonly optionService: OptionService) {} @Post() - @UsePipes(new ValidationPipe({ transform: true })) + @UsePipes(new ValidationPipe({ transform: true, whitelist: true })) async create(@Body() body: CreateOptionDto): Promise