mongo to postgres (formResult and realm left to do)

This commit is contained in:
OkaykOrhmn 2025-07-22 13:05:07 +03:30
parent 3c0f937295
commit 4c3ccaa7b4
52 changed files with 913 additions and 835 deletions

151
package-lock.json generated
View File

@ -15,6 +15,7 @@
"@nestjs/mapped-types": "^2.1.0", "@nestjs/mapped-types": "^2.1.0",
"@nestjs/mongoose": "^11.0.3", "@nestjs/mongoose": "^11.0.3",
"@nestjs/platform-express": "^11.1.3", "@nestjs/platform-express": "^11.1.3",
"@nestjs/typeorm": "^11.0.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"libphonenumber-js": "^1.12.9", "libphonenumber-js": "^1.12.9",
@ -22,6 +23,7 @@
"mongoose": "^8.16.3", "mongoose": "^8.16.3",
"mongoose-paginate-v2": "^1.9.1", "mongoose-paginate-v2": "^1.9.1",
"nestjs-paginate": "^12.5.1", "nestjs-paginate": "^12.5.1",
"pg": "^8.11.3",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2", "rxjs": "^7.8.2",
"uuid": "^11.1.0" "uuid": "^11.1.0"
@ -2670,13 +2672,13 @@
} }
}, },
"node_modules/@nestjs/platform-express": { "node_modules/@nestjs/platform-express": {
"version": "11.1.3", "version": "11.1.5",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.3.tgz", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.5.tgz",
"integrity": "sha512-hEDNMlaPiBO72fxxX/CuRQL3MEhKRc/sIYGVoXjrnw6hTxZdezvvM6A95UaLsYknfmcZZa/CdG1SMBZOu9agHQ==", "integrity": "sha512-OsoiUBY9Shs5IG3uvDIt9/IDfY5OlvWBESuB/K4Eun8xILw1EK5d5qMfC3d2sIJ+kA3l+kBR1d/RuzH7VprLIg==",
"dependencies": { "dependencies": {
"cors": "2.8.5", "cors": "2.8.5",
"express": "5.1.0", "express": "5.1.0",
"multer": "2.0.1", "multer": "2.0.2",
"path-to-regexp": "8.2.0", "path-to-regexp": "8.2.0",
"tslib": "2.8.1" "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": { "node_modules/@noble/hashes": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
@ -7158,9 +7172,9 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.3", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
@ -9350,9 +9364,9 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}, },
"node_modules/multer": { "node_modules/multer": {
"version": "2.0.1", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
"integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
"dependencies": { "dependencies": {
"append-field": "^1.0.0", "append-field": "^1.0.0",
"busboy": "^1.6.0", "busboy": "^1.6.0",
@ -9834,6 +9848,87 @@
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true "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": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -9989,6 +10084,41 @@
"node": ">= 0.4" "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": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -10820,7 +10950,6 @@
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"peer": true,
"engines": { "engines": {
"node": ">= 10.x" "node": ">= 10.x"
} }

View File

@ -26,6 +26,8 @@
"@nestjs/mapped-types": "^2.1.0", "@nestjs/mapped-types": "^2.1.0",
"@nestjs/mongoose": "^11.0.3", "@nestjs/mongoose": "^11.0.3",
"@nestjs/platform-express": "^11.1.3", "@nestjs/platform-express": "^11.1.3",
"@nestjs/typeorm": "^11.0.0",
"pg": "^8.11.3",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.2", "class-validator": "^0.14.2",
"libphonenumber-js": "^1.12.9", "libphonenumber-js": "^1.12.9",

View File

@ -1,18 +1,18 @@
import { IsString, IsEnum, IsOptional, IsUUID, ValidateNested } from 'class-validator'; import { IsString, IsEnum, IsOptional, IsUUID, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import {MetadataDto} from './metadataEntry.dto'; import { MetadataDto } from './metadataEntry.dto';
export class BaseDto { export class BaseDto {
@IsUUID() @IsUUID()
@IsOptional() // id is optional since BaseEntity generates it @IsOptional()
id?: string; id?: string;
@IsEnum(['active', 'inactive']) @IsEnum(['active', 'inactive'])
@IsOptional() @IsOptional()
status?: 'active'| 'inactive'; status?: 'active' | 'inactive';
@ValidateNested() @ValidateNested()
@Type(() => MetadataDto) @Type(() => MetadataDto)
@IsOptional() @IsOptional()
metadata?: MetadataDto; // Optional, defaults to { entries: [] } metadata?: MetadataDto;
} }

View File

@ -14,4 +14,3 @@ export class MetadataDto {
@Type(() => MetadataEntryDto) @Type(() => MetadataEntryDto)
entries: MetadataEntryDto[]; entries: MetadataEntryDto[];
} }

View File

@ -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 { 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 { export abstract class BaseEntity {
@Prop({ @PrimaryGeneratedColumn('uuid')
type: String,
default: () => uuidv4(),
required: true,
unique: true,
immutable: true,
})
id: string; id: string;
@Prop({ @CreateDateColumn()
type: Date,
default: () => new Date(),
required: true,
immutable: true,
})
createdAt: Date; createdAt: Date;
@Prop({ @UpdateDateColumn()
type: Date,
default: () => new Date(),
required: true,
})
updatedAt: Date; updatedAt: Date;
@Prop({ @Column(() => Metadata)
type: MetadataSchema,
default: () => ({ entries: [] }),
required: true,
})
metadata: Metadata; metadata: Metadata;
@Prop({ @Column({
type: String, type: 'enum',
enum: ['active', 'inactive'], enum: ['active', 'inactive'],
default: 'active', default: 'active',
required: true,
}) })
status: 'active' | 'inactive'; status: 'active' | 'inactive';
}
export type BaseEntityDocument = BaseEntity & Document; @BeforeInsert()
generateUuid() {
if (!this.id) {
this.id = uuidv4();
}
}
}

View File

@ -1,28 +1,17 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Column, Entity } from 'typeorm';
import { Document } from 'mongoose';
import { v4 as uuidv4 } from 'uuid';
@Schema({ _id: false }) // Embedded document @Entity({ name: 'metadata' })
export class Metadata { export class Metadata {
@Prop({ @Column({
type: String, type: 'uuid',
default: () => uuidv4(), primary: true,
required: true, default: () => 'uuid_generate_v4()',
immutable: true,
}) })
id: string; id: string;
@Prop({ @Column({
type: [ type: 'jsonb',
{ default: () => "'[]'",
key: { type: String, required: true },
value: { type: String, required: true },
},
],
default: [],
}) })
entries: { key: string; value: string }[]; entries: { key: string; value: string }[];
} }
export type MetadataDocument = Metadata & Document;
export const MetadataSchema = SchemaFactory.createForClass(Metadata);

View File

@ -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 { AnswerService } from './answer.service';
import { CreateAnswerDto } from './dto/create-answer.dto'; import { CreateAnswerDto } from './dto/create-answer.dto';
import { UpdateAnswerDto } from './dto/update-answer.dto'; import { UpdateAnswerDto } from './dto/update-answer.dto';
@ -19,7 +19,6 @@ export class AnswerController {
@Query('page') page: string = '1', @Query('page') page: string = '1',
@Query('limit') limit: string = '10', @Query('limit') limit: string = '10',
) { ) {
// The service returns the full pagination object
return this.answerService.findAll(parseInt(page, 10), parseInt(limit, 10)); 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<void> { async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> {
return this.answerService.remove(id); return this.answerService.remove(id);
} }
} }

View File

@ -1,16 +1,14 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; import { TypeOrmModule } from '@nestjs/typeorm';
import { AnswerService } from './answer.service'; import { AnswerService } from './answer.service';
import { AnswerController } from './answer.controller'; import { AnswerController } from './answer.controller';
import { Answer, AnswerSchema } from './entity/answer.entity'; import { Answer } from './entity/answer.entity';
@Module({ @Module({
imports: [ imports: [
MongooseModule.forFeature([ TypeOrmModule.forFeature([Answer]),
{ name: Answer.name, schema: AnswerSchema },
]),
], ],
controllers: [AnswerController], controllers: [AnswerController],
providers: [AnswerService], providers: [AnswerService],
}) })
export class AnswerModule {} export class AnswerModule {}

View File

@ -1,25 +1,28 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectRepository } from '@nestjs/typeorm';
import { Model } from 'mongoose'; import { Repository } from 'typeorm';
import { Answer, AnswerDocument } from './entity/answer.entity'; import { Answer } from './entity/answer.entity';
import { CreateAnswerDto } from './dto/create-answer.dto'; import { CreateAnswerDto } from './dto/create-answer.dto';
import { UpdateAnswerDto } from './dto/update-answer.dto'; import { UpdateAnswerDto } from './dto/update-answer.dto';
import { PaginateModel } from '../participant/participant.service';
@Injectable() @Injectable()
export class AnswerService { export class AnswerService {
constructor( constructor(
@InjectModel(Answer.name) @InjectRepository(Answer)
private readonly answerModel: PaginateModel<AnswerDocument>, private readonly answerRepository: Repository<Answer>,
) {} ) {}
async create(data: CreateAnswerDto): Promise<Answer> { async create(data: CreateAnswerDto): Promise<Answer> {
const answer = new this.answerModel({ try {
...data, const answer = this.answerRepository.create({
id: data.id || undefined, // Let BaseEntity generate UUID if not provided ...data,
metadata: data.metadata || { entries: [] }, metadata: data.metadata || { entries: [] },
}); status: data.status || 'active',
return answer.save(); });
return await this.answerRepository.save(answer);
} catch (error) {
throw new Error(`Failed to create answer: ${error.message}`);
}
} }
async findAll( async findAll(
@ -36,46 +39,62 @@ export class AnswerService {
nextPage: number | null; nextPage: number | null;
prevPage: number | null; prevPage: number | null;
}> { }> {
return this.answerModel.paginate( try {
{}, const skip = (page - 1) * limit;
{ page, limit, lean: true }, 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<Answer | null> { async findById(id: string): Promise<Answer | null> {
const answer = await this.answerModel try {
.findOne({ id }) const answer = await this.answerRepository.findOne({ where: { id } });
.lean() if (!answer) {
.exec(); throw new NotFoundException(`Answer with ID "${id}" not found`);
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( async update(id: string, data: UpdateAnswerDto): Promise<Answer | null> {
id: string, try {
data: UpdateAnswerDto, await this.answerRepository.update({ id }, { ...data, updatedAt: new Date() });
): Promise<Answer | null> { const updatedAnswer = await this.answerRepository.findOne({ where: { id } });
const updatedAnswer = await this.answerModel if (!updatedAnswer) {
.findOneAndUpdate( throw new NotFoundException(`Answer with ID "${id}" not found`);
{ id }, }
{ $set: { ...data, updatedAt: new Date() } }, return updatedAnswer;
{ new: true }, } catch (error) {
) throw new Error(`Failed to update answer: ${error.message}`);
.lean()
.exec();
if (!updatedAnswer) {
throw new NotFoundException(`Answer with ID "${id}" not found`);
} }
return updatedAnswer;
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
const result = await this.answerModel.deleteOne({ id }).exec(); try {
if (result.deletedCount === 0) { const result = await this.answerRepository.delete({ id });
throw new NotFoundException(`Answer with ID "${id}" not found`); if (result.affected === 0) {
throw new NotFoundException(`Answer with ID "${id}" not found`);
}
} catch (error) {
throw new Error(`Failed to delete answer: ${error.message}`);
} }
} }
} }

View File

@ -1,7 +1,7 @@
import { IsString, IsUUID} from 'class-validator'; import { IsString, IsUUID } from 'class-validator';
import { BaseDto } from '../../_core/dto/base.dto'; import { BaseDto } from '../../_core/dto/base.dto';
export class CreateAnswerDto extends BaseDto{ export class CreateAnswerDto extends BaseDto {
@IsUUID() @IsUUID()
participantId: string; participantId: string;
@ -10,4 +10,4 @@ export class CreateAnswerDto extends BaseDto{
@IsString() @IsString()
value: string; value: string;
} }

View File

@ -1,44 +1,22 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Column, Entity, ManyToOne } from 'typeorm';
import { Document } from 'mongoose';
import { BaseEntity } from 'src/_core/entity/_base.entity'; import { BaseEntity } from 'src/_core/entity/_base.entity';
import * as mongoosePaginate from 'mongoose-paginate-v2'; import { Question } from '../../question/entity/question.entity';
import { v4 as uuidv4 } from 'uuid';
@Schema({ _id: false, id: false }) // Disable _id and virtual id @Entity({ name: 'answers' })
export class Answer extends BaseEntity { export class Answer extends BaseEntity {
@Prop({ @Column({
type: String, type: 'uuid',
default: () => uuidv4(),
required: true,
immutable: true,
}) })
participantId: string; participantId: string;
@Prop({ @Column({
type: String, type: 'uuid',
default: () => uuidv4(),
required: true,
immutable: true,
}) })
questionId: string; // form can be identified based on question questionId: string;
@Prop({ @Column()
type: String, value: string;
required: true,
})
value: string; //value can be mapped to option based on question type
@ManyToOne(() => Question, question => question.answers)
} question: Question;
}
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

@ -1,6 +1,5 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { ParticipantModule } from './participant/participant.module'; import { ParticipantModule } from './participant/participant.module';
@ -14,13 +13,15 @@ import { FormSectionModule } from './formSection/formSection.module';
import { OptionModule } from './option/option.module'; import { OptionModule } from './option/option.module';
import { ParticipanGrouptModule } from './participantGroup/participantGroup.module'; import { ParticipanGrouptModule } from './participantGroup/participantGroup.module';
import { RealmModule } from './realm/realm.module'; import { RealmModule } from './realm/realm.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { typeOrmConfig } from './config/database.config';
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forRoot(typeOrmConfig),
ConfigModule.forRoot({ ConfigModule.forRoot({
isGlobal: true, // Makes ConfigModule available globally isGlobal: true, // Makes ConfigModule available globally
}), }),
MongooseModule.forRoot(process.env.MONGODB_URI || 'mongodb://localhost:27017/db'),
ParticipantModule, ParticipantModule,
QuestionModule, QuestionModule,
AnswerModule, AnswerModule,

View File

@ -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 { AttachmentService } from './attachment.service';
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';
@ -19,7 +19,6 @@ export class AttachmentController {
@Query('page') page: string = '1', @Query('page') page: string = '1',
@Query('limit') limit: string = '10', @Query('limit') limit: string = '10',
) { ) {
// The service returns the full pagination object
return this.attachmentService.findAll(parseInt(page, 10), parseInt(limit, 10)); 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<void> { async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> {
return this.attachmentService.remove(id); return this.attachmentService.remove(id);
} }
} }

View File

@ -1,16 +1,14 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; import { TypeOrmModule } from '@nestjs/typeorm';
import { Attachment, AttachmentSchema } from './entity/attachment.entity'; import { Attachment } from './entity/attachment.entity';
import { AttachmentController } from './attachment.controller'; import { AttachmentController } from './attachment.controller';
import { AttachmentService } from './attachment.service'; import { AttachmentService } from './attachment.service';
@Module({ @Module({
imports: [ imports: [
MongooseModule.forFeature([ TypeOrmModule.forFeature([Attachment]),
{ name: Attachment.name, schema: AttachmentSchema },
]),
], ],
controllers: [AttachmentController], controllers: [AttachmentController],
providers: [AttachmentService], providers: [AttachmentService],
}) })
export class AttachmentModule {} export class AttachmentModule {}

View File

@ -1,26 +1,28 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectRepository } from '@nestjs/typeorm';
import { Model } from 'mongoose'; import { Repository } from 'typeorm';
import { Attachment, AttachmentDocument } from './entity/attachment.entity'; import { Attachment } 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';
@Injectable() @Injectable()
export class AttachmentService { export class AttachmentService {
constructor( constructor(
@InjectModel(Attachment.name) @InjectRepository(Attachment)
private readonly attachmentModel: PaginateModel<AttachmentDocument>, private readonly attachmentRepository: Repository<Attachment>,
) {} ) {}
async create(data: CreateAttachmentDto): Promise<Attachment> { async create(data: CreateAttachmentDto): Promise<Attachment> {
const attachment = new this.attachmentModel({ try {
...data, const attachment = this.attachmentRepository.create({
id: data.id || undefined, // Let BaseEntity generate UUID if not provided ...data,
metadata: data.metadata || { entries: [] }, metadata: data.metadata || { entries: [] },
}); status: data.status || 'active',
return attachment.save(); });
return await this.attachmentRepository.save(attachment);
} catch (error) {
throw new Error(`Failed to create attachment: ${error.message}`);
}
} }
async findAll( async findAll(
@ -37,47 +39,62 @@ export class AttachmentService {
nextPage: number | null; nextPage: number | null;
prevPage: number | null; prevPage: number | null;
}> { }> {
// Selects all fields from the Attachment entity for the response try {
return this.attachmentModel.paginate( const skip = (page - 1) * limit;
{}, const [docs, totalDocs] = await this.attachmentRepository.findAndCount({
{ page, limit, lean: true }, 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<Attachment | null> { async findById(id: string): Promise<Attachment | null> {
const attachment = await this.attachmentModel try {
.findOne({ id }) const attachment = await this.attachmentRepository.findOne({ where: { id } });
.lean() if (!attachment) {
.exec(); throw new NotFoundException(`Attachment with ID "${id}" not found`);
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( async update(id: string, data: UpdateAttachmentDto): Promise<Attachment | null> {
id: string, try {
data: UpdateAttachmentDto, await this.attachmentRepository.update({ id }, { ...data, updatedAt: new Date() });
): Promise<Attachment | null> { const updatedAttachment = await this.attachmentRepository.findOne({ where: { id } });
const updatedAttachment = await this.attachmentModel if (!updatedAttachment) {
.findOneAndUpdate( throw new NotFoundException(`Attachment with ID "${id}" not found`);
{ id }, }
{ $set: { ...data, updatedAt: new Date() } }, return updatedAttachment;
{ new: true }, } catch (error) {
) throw new Error(`Failed to update attachment: ${error.message}`);
.lean()
.exec();
if (!updatedAttachment) {
throw new NotFoundException(`Attachment with ID "${id}" not found`);
} }
return updatedAttachment;
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
const result = await this.attachmentModel.deleteOne({ id }).exec(); try {
if (result.deletedCount === 0) { const result = await this.attachmentRepository.delete({ id });
throw new NotFoundException(`Attachment with ID "${id}" not found`); if (result.affected === 0) {
throw new NotFoundException(`Attachment with ID "${id}" not found`);
}
} catch (error) {
throw new Error(`Failed to delete attachment: ${error.message}`);
} }
} }
} }

View File

@ -1,17 +1,16 @@
// DTO for embedded Attachment import { IsEnum, IsOptional, IsString, IsUrl, IsUUID, ValidateNested } from 'class-validator';
import { isEnum, IsEnum, IsOptional, IsString, IsUrl, IsUUID, ValidateNested } from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { BaseDto } from '../../_core/dto/base.dto'; import { BaseDto } from '../../_core/dto/base.dto';
export class CreateAttachmentDto extends BaseDto{ export class CreateAttachmentDto extends BaseDto {
@IsString() @IsString()
displayName: string; displayName: string;
@IsString() @IsString()
filePath: string; filePath: string;
@IsEnum(['png','pdf','csv','jpg']) @IsEnum(['png', 'pdf', 'csv', 'jpg'])
fileType: 'png'|'pdf'|'csv'|'jpg'; fileType: 'png' | 'pdf' | 'csv' | 'jpg';
@IsUrl() @IsUrl()
storageUrl: string; storageUrl: string;

View File

@ -1,40 +1,33 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Column, Entity, ManyToOne } from 'typeorm';
import { Document } from 'mongoose';
import { BaseEntity } from 'src/_core/entity/_base.entity'; 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 { export class Attachment extends BaseEntity {
@Prop({ @Column()
type: String,
required: true,
})
displayName: string; displayName: string;
@Prop({ @Column()
type: String,
required: true,
})
filePath: string; filePath: string;
@Prop({ @Column({
type: String, type: 'enum',
enum: ['png', 'pdf', 'csv', 'jpg'], enum: ['png', 'pdf', 'csv', 'jpg'],
required: true,
}) })
fileType: 'png' | 'pdf' | 'csv' | 'jpg'; fileType: 'png' | 'pdf' | 'csv' | 'jpg';
@Prop({ @Column()
type: String,
required: true,
})
storageUrl: string; storageUrl: string;
@Prop({ @Column({
type: String, type: 'uuid',
required: true,
}) })
ownerId: string; // UUID of the user who uploaded it ownerId: string;
}
export type AttachmentDocument = Attachment & Document; @ManyToOne(() => Question, (question) => question.attachments)
export const AttachmentSchema = SchemaFactory.createForClass(Attachment); question: Question;
@ManyToOne(() => FormSection, formSection => formSection.attachments)
formSection: FormSection;
}

View File

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

View File

@ -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 { Type } from 'class-transformer';
import { BaseDto } from '../../_core/dto/base.dto'; 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 { CreateFormPageDto } from '../../formPage/dto/create-formPage.dto';
import { CreateParticipantDto } from '../../participant/dto/create-participant.dto'; import { CreateParticipantDto } from '../../participant/dto/create-participant.dto';
import { CreateAttachmentDto } from '../../attachment/dto/create_attachment.dto';
export class CreateFormDto extends BaseDto { export class CreateFormDto extends BaseDto {
@IsString() @IsString()
@ -17,7 +16,7 @@ export class CreateFormDto extends BaseDto {
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => CreateAttachmentDto) @Type(() => CreateAttachmentDto)
@IsOptional() @IsOptional()
attachments?: CreateAttachmentDto[]; attachments?: CreateAttachmentDto[]; // Keep if attachments are still part of the DTO
@IsArray() @IsArray()
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@ -28,4 +27,4 @@ export class CreateFormDto extends BaseDto {
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => CreateParticipantDto) @Type(() => CreateParticipantDto)
participants: CreateParticipantDto[]; participants: CreateParticipantDto[];
} }

View File

@ -1,58 +1,39 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Entity, Column, OneToMany } from 'typeorm';
import { Document } from 'mongoose'; import { BaseEntity } from '../../_core/entity/_base.entity';
import { BaseEntity } from 'src/_core/entity/_base.entity'; import { FormPage } from '../../formPage/entity/formPage.entity';
import * as mongoosePaginate from 'mongoose-paginate-v2'; import { Participant } from '../../participant/entity/participant.entity'; // Assuming Participant entity exists
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';
@Schema({ _id: false, id: false }) // Disable _id and virtual id @Entity()
export class Form extends BaseEntity { export class Form extends BaseEntity {
@Prop({ @Column()
type: String,
required: true,
})
title: string; title: string;
@Prop({ @Column()
type: String,
required: true,
})
description: string; description: string;
@Prop({ // One-to-Many relationship with FormPage
type: [AttachmentSchema], @OneToMany(() => FormPage, formPage => formPage.form, { cascade: true, eager: true })
default: [],
})
attachments: Attachment[];
@Prop({
type: [FormPageSchema],
default: [],
})
pages: FormPage[]; pages: FormPage[];
@Prop({ // One-to-Many relationship with Participant
type: [ParticipantSchema], @OneToMany(() => Participant, participant => participant.form, { cascade: true, eager: true })
default: [],
})
participants: Participant[]; 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;
},
});

View File

@ -19,7 +19,6 @@ export class FormController {
@Query('page') page: string = '1', @Query('page') page: string = '1',
@Query('limit') limit: string = '10', @Query('limit') limit: string = '10',
) { ) {
// The service returns the full pagination object
return this.formService.findAll(parseInt(page, 10), parseInt(limit, 10)); return this.formService.findAll(parseInt(page, 10), parseInt(limit, 10));
} }

View File

@ -1,13 +1,27 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; import { TypeOrmModule } from '@nestjs/typeorm';
import { FormService } from './form.service'; import { FormService } from './form.service';
import { FormController } from './form.controller'; 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({ @Module({
imports: [ imports: [
MongooseModule.forFeature([ TypeOrmModule.forFeature([
{ name: Form.name, schema: FormSchema }, Form,
FormPage,
FormSection,
Attachment,
Question,
Answer,
Option,
Participant, // Include Participant entity
]), ]),
], ],
controllers: [FormController], controllers: [FormController],

View File

@ -1,25 +1,22 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectRepository } from '@nestjs/typeorm';
import { Model } from 'mongoose'; import { Repository } from 'typeorm';
import { Form, FormDocument } from './entity/form.entity'; import { Form } from './entity/form.entity';
import { CreateFormDto } from './dto/create-form.dto'; import { CreateFormDto } from './dto/create-form.dto';
import { UpdateFormDto } from './dto/update-form.dto'; import { UpdateFormDto } from './dto/update-form.dto';
import { PaginateModel } from '../participant/participant.service';
@Injectable() @Injectable()
export class FormService { export class FormService {
constructor( constructor(
@InjectModel(Form.name) @InjectRepository(Form)
private readonly formModel: PaginateModel<FormDocument>, private readonly formRepo: Repository<Form>,
) {} ) {}
async create(data: CreateFormDto): Promise<Form> { async create(data: CreateFormDto): Promise<Form> {
const form = new this.formModel({ const form = this.formRepo.create({
...data, ...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( async findAll(
@ -36,18 +33,47 @@ export class FormService {
nextPage: number | null; nextPage: number | null;
prevPage: number | null; prevPage: number | null;
}> { }> {
// Selects all fields from the Form entity for the response const [docs, totalDocs] = await this.formRepo.findAndCount({
return this.formModel.paginate( skip: (page - 1) * limit,
{}, take: limit,
{ page, limit, lean: true }, 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<Form | null> { async findById(id: string): Promise<Form | null> {
const form = await this.formModel const form = await this.formRepo.findOne({
.findOne({ id }) where: { id },
.lean() relations: [
.exec(); 'pages',
'pages.formSections',
'pages.formSections.attachments',
'pages.formSections.questions',
'pages.formSections.questions.options',
'pages.formSections.questions.answers',
'participants',
], // Load nested relations
});
if (!form) { if (!form) {
throw new NotFoundException(`Form with ID "${id}" not found`); throw new NotFoundException(`Form with ID "${id}" not found`);
} }
@ -58,24 +84,20 @@ export class FormService {
id: string, id: string,
data: UpdateFormDto, data: UpdateFormDto,
): Promise<Form | null> { ): Promise<Form | null> {
const updatedForm = await this.formModel const result = await this.formRepo.update(
.findOneAndUpdate( { id },
{ id }, { ...data, updatedAt: new Date() },
{ $set: { ...data, updatedAt: new Date() } }, );
{ new: true },
)
.lean()
.exec();
if (!updatedForm) { if (result.affected === 0) {
throw new NotFoundException(`Form with ID "${id}" not found`); throw new NotFoundException(`Form with ID "${id}" not found`);
} }
return updatedForm; return await this.findById(id);
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
const result = await this.formModel.deleteOne({ id }).exec(); const result = await this.formRepo.delete({ id });
if (result.deletedCount === 0) { if (result.affected === 0) {
throw new NotFoundException(`Form with ID "${id}" not found`); throw new NotFoundException(`Form with ID "${id}" not found`);
} }
} }

View File

@ -14,4 +14,4 @@ export class CreateFormPageDto extends BaseDto {
@ValidateNested({ each: true }) @ValidateNested({ each: true })
@Type(() => CreateFormSectionDto) @Type(() => CreateFormSectionDto)
formSections: CreateFormSectionDto[]; formSections: CreateFormSectionDto[];
} }

View File

@ -1,45 +1,22 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Entity, Column, OneToMany, ManyToOne } from 'typeorm';
import { Document } from 'mongoose'; import { BaseEntity } from '../../_core/entity/_base.entity';
import { BaseEntity } from 'src/_core/entity/_base.entity'; import { FormSection } from '../../formSection/entity/formSection.entity';
import * as mongoosePaginate from 'mongoose-paginate-v2'; import { Form } from '../../form/entity/form.entity';
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';
@Schema({ _id: false, id: false }) // Disable _id and virtual id @Entity()
export class FormPage extends BaseEntity { export class FormPage extends BaseEntity {
@Prop({ @Column()
type: String,
required: true,
})
title: string; title: string;
@Prop({ @Column()
type: String,
required: true,
})
description: string; description: string;
@Prop({ // One-to-Many relationship with FormSection
type: [FormSectionSchema], @OneToMany(() => FormSection, formSection => formSection.formPage, { cascade: true, eager: true })
default: [], formSections: FormSection[];
})
attachments: 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;
},
});

View File

@ -19,7 +19,6 @@ export class FormPageController {
@Query('page') page: string = '1', @Query('page') page: string = '1',
@Query('limit') limit: string = '10', @Query('limit') limit: string = '10',
) { ) {
// The service returns the full pagination object
return this.formPageService.findAll(parseInt(page, 10), parseInt(limit, 10)); return this.formPageService.findAll(parseInt(page, 10), parseInt(limit, 10));
} }

View File

@ -1,13 +1,23 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; import { TypeOrmModule } from '@nestjs/typeorm';
import { FormPageService } from './formPage.service'; import { FormPageService } from './formPage.service';
import { FormPageController } from './formPage.controller'; 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({ @Module({
imports: [ imports: [
MongooseModule.forFeature([ TypeOrmModule.forFeature([
{ name: FormPage.name, schema: FormPageSchema }, FormPage,
FormSection,
Attachment,
Question,
Answer,
Option,
]), ]),
], ],
controllers: [FormPageController], controllers: [FormPageController],

View File

@ -1,25 +1,22 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectRepository } from '@nestjs/typeorm';
import { Model } from 'mongoose'; import { Repository } from 'typeorm';
import { FormPage, FormPageDocument } from './entity/formPage.entity'; import { FormPage } from './entity/formPage.entity';
import { CreateFormPageDto } from './dto/create-formPage.dto'; import { CreateFormPageDto } from './dto/create-formPage.dto';
import { UpdateFormPageDto } from './dto/update-formPage.dto'; import { UpdateFormPageDto } from './dto/update-formPage.dto';
import { PaginateModel } from '../participant/participant.service';
@Injectable() @Injectable()
export class FormPageService { export class FormPageService {
constructor( constructor(
@InjectModel(FormPage.name) @InjectRepository(FormPage)
private readonly formPageModel: PaginateModel<FormPageDocument>, private readonly formPageRepo: Repository<FormPage>,
) {} ) {}
async create(data: CreateFormPageDto): Promise<FormPage> { async create(data: CreateFormPageDto): Promise<FormPage> {
const formPage = new this.formPageModel({ const formPage = this.formPageRepo.create({
...data, ...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( async findAll(
@ -36,18 +33,31 @@ export class FormPageService {
nextPage: number | null; nextPage: number | null;
prevPage: number | null; prevPage: number | null;
}> { }> {
// Selects all fields from the FormPage entity for the response const [docs, totalDocs] = await this.formPageRepo.findAndCount({
return this.formPageModel.paginate( skip: (page - 1) * limit,
{}, take: limit,
{ page, limit, lean: true }, 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<FormPage | null> { async findById(id: string): Promise<FormPage | null> {
const formPage = await this.formPageModel const formPage = await this.formPageRepo.findOne({
.findOne({ id }) where: { id },
.lean() relations: ['formSections', 'formSections.attachments', 'formSections.questions', 'formSections.questions.options', 'formSections.questions.answers'], // Load related data
.exec(); });
if (!formPage) { if (!formPage) {
throw new NotFoundException(`FormPage with ID "${id}" not found`); throw new NotFoundException(`FormPage with ID "${id}" not found`);
} }
@ -58,24 +68,20 @@ export class FormPageService {
id: string, id: string,
data: UpdateFormPageDto, data: UpdateFormPageDto,
): Promise<FormPage | null> { ): Promise<FormPage | null> {
const updatedFormPage = await this.formPageModel const result = await this.formPageRepo.update(
.findOneAndUpdate( { id },
{ id }, { ...data, updatedAt: new Date() },
{ $set: { ...data, updatedAt: new Date() } }, );
{ new: true },
)
.lean()
.exec();
if (!updatedFormPage) { if (result.affected === 0) {
throw new NotFoundException(`FormPage with ID "${id}" not found`); throw new NotFoundException(`FormPage with ID "${id}" not found`);
} }
return updatedFormPage; return await this.findById(id);
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
const result = await this.formPageModel.deleteOne({ id }).exec(); const result = await this.formPageRepo.delete({ id });
if (result.deletedCount === 0) { if (result.affected === 0) {
throw new NotFoundException(`FormPage with ID "${id}" not found`); throw new NotFoundException(`FormPage with ID "${id}" not found`);
} }
} }

View File

@ -1,85 +1,46 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; // src/formSection/entity/formSection.entity.ts
import { Document } from 'mongoose'; import { Entity, Column, OneToMany, ManyToOne } from 'typeorm'; // Removed Embeddable import
import { BaseEntity } from 'src/_core/entity/_base.entity'; import { BaseEntity } from '../../_core/entity/_base.entity';
import * as mongoosePaginate from 'mongoose-paginate-v2'; import { Attachment } from '../../attachment/entity/attachment.entity';
import { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity'; import { Question } from '../../question/entity/question.entity';
import { Option, OptionSchema } from '../../option/entity/option.entity'; import { FormPage } from 'src/formPage/entity/formPage.entity';
import { Answer, AnswerSchema } from '../../answer/entity/answer.entity';
import { Question, QuestionSchema } from '../../question/entity/question.entity';
// Sub-schema for DisplayContition // DisplayCondition is now a regular class, embedded using @Column(() => DisplayCondition)
@Schema({ _id: false, id: false })
export class DisplayCondition { export class DisplayCondition {
@Prop({ @Column()
type: [AnswerSchema], answer: string; // Assuming this is an ID reference to an Answer entity
required: true,
})
answer: Answer;
@Prop({ @Column()
type: [QuestionSchema], question: string; // Assuming this is an ID reference to a Question entity
required: true,
})
question: Question;
@Prop({ @Column({
type: String, type: 'enum',
enum: ['equal','contains','not_equal','not_contains'], enum: ['equal', 'contains', 'not_equal', 'not_contains'],
required: true,
}) })
relation: 'equal'|'contains'|'not_equal'|'not_contains'; relation: 'equal' | 'contains' | 'not_equal' | 'not_contains';
} }
export const DisplayConditionSchema = SchemaFactory.createForClass(DisplayCondition); @Entity()
//\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
@Schema({ _id: false, id: false }) // Disable _id and virtual id
export class FormSection extends BaseEntity { export class FormSection extends BaseEntity {
@Prop({ @Column()
type: String,
required: true,
})
title: string; title: string;
@Prop({ @Column()
type: String,
required: true,
})
description: string; description: string;
@Prop({ // One-to-Many relationship with Attachment
type: [AttachmentSchema], @OneToMany(() => Attachment, attachment => attachment.formSection, { cascade: true, eager: true })
default: [],
})
attachments: Attachment[]; attachments: Attachment[];
@Prop({ // Using @Column(() => DisplayCondition) for embedded array of objects
type: [DisplayConditionSchema], @Column(() => DisplayCondition)
default: [],
})
displayCondition: DisplayCondition[]; displayCondition: DisplayCondition[];
@Prop({ // One-to-Many relationship with Question
type: [QuestionSchema], @OneToMany(() => Question, question => question.formSection, { cascade: true, eager: true })
default: [],
})
questions: Question[]; questions: Question[];
} // Many-to-One relationship with FormPage
@ManyToOne(() => FormPage, formPage => formPage.formSections)
export type FormSectionDocument = FormSection & Document; formPage: FormPage;
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;
},
});

View File

@ -44,4 +44,4 @@ export class FormSectionController {
async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> { async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> {
return this.formSectionService.remove(id); return this.formSectionService.remove(id);
} }
} }

View File

@ -1,13 +1,22 @@
// src/formSection/formSection.module.ts
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; import { TypeOrmModule } from '@nestjs/typeorm';
import { FormSectionService } from './formSection.service'; import { FormSectionService } from './formSection.service';
import { FormSectionController } from './formSection.controller'; 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({ @Module({
imports: [ imports: [
MongooseModule.forFeature([ TypeOrmModule.forFeature([
{ name: FormSection.name, schema: FormSectionSchema }, FormSection,
Attachment,
Question,
Answer,
Option,
]), ]),
], ],
controllers: [FormSectionController], controllers: [FormSectionController],

View File

@ -1,25 +1,22 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectRepository } from '@nestjs/typeorm';
import { Model } from 'mongoose'; import { Repository } from 'typeorm';
import { FormSection, FormSectionDocument } from './entity/formSection.entity'; import { FormSection } from './entity/formSection.entity';
import { CreateFormSectionDto } from './dto/create-formSection.dto'; import { CreateFormSectionDto } from './dto/create-formSection.dto';
import { UpdateFormSectionDto } from './dto/update-formSection.dto'; import { UpdateFormSectionDto } from './dto/update-formSection.dto';
import { PaginateModel } from '../participant/participant.service';
@Injectable() @Injectable()
export class FormSectionService { export class FormSectionService {
constructor( constructor(
@InjectModel(FormSection.name) @InjectRepository(FormSection)
private readonly formSectionModel: PaginateModel<FormSectionDocument>, private readonly formSectionRepo: Repository<FormSection>,
) {} ) {}
async create(data: CreateFormSectionDto): Promise<FormSection> { async create(data: CreateFormSectionDto): Promise<FormSection> {
const formSection = new this.formSectionModel({ const formSection = this.formSectionRepo.create({
...data, ...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( async findAll(
@ -36,18 +33,31 @@ export class FormSectionService {
nextPage: number | null; nextPage: number | null;
prevPage: number | null; prevPage: number | null;
}> { }> {
// Selects all fields from the FormSection entity for the response const [docs, totalDocs] = await this.formSectionRepo.findAndCount({
return this.formSectionModel.paginate( skip: (page - 1) * limit,
{}, take: limit,
{ page, limit, lean: true }, 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<FormSection | null> { async findById(id: string): Promise<FormSection | null> {
const formSection = await this.formSectionModel const formSection = await this.formSectionRepo.findOne({
.findOne({ id }) where: { id },
.lean() relations: ['attachments', 'questions', 'questions.options', 'questions.answers'], // Load related data
.exec(); });
if (!formSection) { if (!formSection) {
throw new NotFoundException(`FormSection with ID "${id}" not found`); throw new NotFoundException(`FormSection with ID "${id}" not found`);
} }
@ -58,24 +68,23 @@ export class FormSectionService {
id: string, id: string,
data: UpdateFormSectionDto, data: UpdateFormSectionDto,
): Promise<FormSection | null> { ): Promise<FormSection | null> {
const updatedFormSection = await this.formSectionModel // TypeORM's update method returns UpdateResult, not the entity itself.
.findOneAndUpdate( // We update and then fetch the updated entity.
{ id }, const result = await this.formSectionRepo.update(
{ $set: { ...data, updatedAt: new Date() } }, { id },
{ new: true }, { ...data, updatedAt: new Date() },
) );
.lean()
.exec();
if (!updatedFormSection) { if (result.affected === 0) {
throw new NotFoundException(`FormSection with ID "${id}" not found`); 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<void> { async remove(id: string): Promise<void> {
const result = await this.formSectionModel.deleteOne({ id }).exec(); const result = await this.formSectionRepo.delete({ id });
if (result.deletedCount === 0) { if (result.affected === 0) {
throw new NotFoundException(`FormSection with ID "${id}" not found`); throw new NotFoundException(`FormSection with ID "${id}" not found`);
} }
} }

View File

@ -3,6 +3,7 @@ import { AppModule } from './app.module';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.enableCors({ origin: 'http://localhost:3001' });
await app.listen(process.env.PORT ?? 3000); await app.listen(process.env.PORT ?? 3000);
} }
bootstrap(); bootstrap();

View File

@ -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'; import { BaseDto } from '../../_core/dto/base.dto';
// DTO for embedded Option export class CreateOptionDto extends BaseDto {
export class CreateOptionDto extends BaseDto{
@IsString() @IsString()
text: string; text: string;
} }

View File

@ -1,15 +1,14 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Column, Entity, ManyToOne } from 'typeorm';
import { Document } from 'mongoose';
import { BaseEntity } from 'src/_core/entity/_base.entity'; 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 { export class Option extends BaseEntity {
@Prop({ @Column()
type: String,
required: true,
})
text: string; text: string;
}
export type OptionDocument = Option & Document; @ManyToOne(() => Question, (question) => question.options)
export const OptionSchema = SchemaFactory.createForClass(Option); question: Question;
}

View File

@ -1,17 +1,15 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query } from '@nestjs/common'; import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus } from '@nestjs/common';
import { ParseUUIDPipe } from '@nestjs/common';
import { OptionService } from './option.service'; import { OptionService } from './option.service';
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 } from './entity/option.entity'; import { Option } from './entity/option.entity';
import { Paginate, Paginated, PaginateQuery } from 'nestjs-paginate';
@Controller('options') @Controller('options')
export class OptionController { export class OptionController {
constructor(private readonly optionService: OptionService) {} constructor(private readonly optionService: OptionService) {}
@Post() @Post()
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async create(@Body() body: CreateOptionDto): Promise<Option> { async create(@Body() body: CreateOptionDto): Promise<Option> {
return this.optionService.create(body); return this.optionService.create(body);
} }
@ -21,25 +19,25 @@ export class OptionController {
@Query('page') page: string = '1', @Query('page') page: string = '1',
@Query('limit') limit: string = '10', @Query('limit') limit: string = '10',
) { ) {
// The service returns the full pagination object
return this.optionService.findAll(parseInt(page, 10), parseInt(limit, 10)); return this.optionService.findAll(parseInt(page, 10), parseInt(limit, 10));
} }
@Get(':id') @Get(':id')
async findOne(@Param('id', new ParseUUIDPipe()) id: string): Promise<Option | null> { async findOne(@Param('id', new ParseUUIDPipe()) id: string): Promise<Option> {
return this.optionService.findById(id); return this.optionService.findById(id);
} }
@Patch(':id') @Patch(':id')
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
async update( async update(
@Param('id', new ParseUUIDPipe()) id: string, @Param('id', new ParseUUIDPipe()) id: string,
@Body() body: UpdateOptionDto, @Body() body: UpdateOptionDto,
): Promise<Option | null> { ): Promise<Option> {
return this.optionService.update(id, body); return this.optionService.update(id, body);
} }
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> { async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> {
return this.optionService.remove(id); return this.optionService.remove(id);
} }

View File

@ -1,12 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; import { TypeOrmModule } from '@nestjs/typeorm';
import { OptionService } from './option.service'; import { OptionService } from './option.service';
import { OptionController } from './option.controller'; import { OptionController } from './option.controller';
import { Option, OptionSchema } from './entity/option.entity'; import { Option } from './entity/option.entity';
@Module({ @Module({
imports: [ imports: [
MongooseModule.forFeature([{ name: Option.name, schema: OptionSchema }]), TypeOrmModule.forFeature([Option]),
], ],
controllers: [OptionController], controllers: [OptionController],
providers: [OptionService], providers: [OptionService],

View File

@ -1,23 +1,34 @@
import { Injectable } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
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 } from './entity/option.entity';
import { PaginateModel } from '../participant/participant.service';
@Injectable() @Injectable()
export class OptionService { export class OptionService {
constructor( constructor(
@InjectModel(Option.name) @InjectRepository(Option)
private readonly optionModel: PaginateModel<OptionDocument>, private readonly optionRepository: Repository<Option>,
) {} ) {}
async create(data: CreateOptionDto): Promise<Option> { async create(data: CreateOptionDto): Promise<Option> {
const option = new this.optionModel({ ...data, id: require('uuid').v4() }); try {
return option.save(); const option = this.optionRepository.create({
...data,
metadata: data.metadata || { entries: [] },
status: data.status || 'active',
});
return await this.optionRepository.save(option);
} catch (error) {
throw new Error(`Failed to create option: ${error.message}`);
}
} }
async findAll(page = 1, limit = 10): Promise<{ async findAll(
page = 1,
limit = 10,
): Promise<{
docs: Option[]; docs: Option[];
totalDocs: number; totalDocs: number;
limit: number; limit: number;
@ -28,25 +39,49 @@ export class OptionService {
nextPage: number | null; nextPage: number | null;
prevPage: number | null; prevPage: number | null;
}> { }> {
return this.optionModel.paginate( try {
{}, const skip = (page - 1) * limit;
{ page, limit, lean: true }, const [docs, totalDocs] = await this.optionRepository.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 options: ${error.message}`);
}
} }
async findById(id: string): Promise<Option> {
async findById(id: string): Promise<Option | null> { const option = await this.optionRepository.findOne({ where: { id } });
return this.optionModel.findOne({ id }).lean().exec(); if (!option) {
throw new NotFoundException(`Option with ID "${id}" not found`);
}
return option;
} }
async update(id: string, data: UpdateOptionDto): Promise<Option | null> { async update(id: string, data: UpdateOptionDto): Promise<Option> {
return this.optionModel const result = await this.optionRepository.update({ id }, { ...data, updatedAt: new Date() });
.findOneAndUpdate({ id }, { ...data, updatedAt: new Date() }, { new: true }) if (result.affected === 0) {
.lean() throw new NotFoundException(`Option with ID "${id}" not found`);
.exec(); }
return await this.findById(id);
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
await this.optionModel.deleteOne({ id }).exec(); const result = await this.optionRepository.delete({ id });
if (result.affected === 0) {
throw new NotFoundException(`Option with ID "${id}" not found`);
}
} }
} }

View File

@ -3,15 +3,11 @@ import { Type } from 'class-transformer';
import { MetadataDto } from '../../_core/dto/metadataEntry.dto'; import { MetadataDto } from '../../_core/dto/metadataEntry.dto';
import { BaseDto } from '../../_core/dto/base.dto'; import { BaseDto } from '../../_core/dto/base.dto';
export class CreateParticipantDto extends BaseDto{ export class CreateParticipantDto extends BaseDto {
@IsUUID()
userId: string; // Required, provided by client
@IsString() @IsString()
displayName: string; // Required, provided by client displayName: string;
@IsEnum(['moderator', 'tester', 'admin', 'user']) @IsEnum(['moderator', 'tester', 'admin', 'user'])
@IsOptional() @IsOptional()
role?: 'moderator' | 'tester' | 'admin' | 'user'; // Optional, defaults to 'user' role?: 'moderator' | 'tester' | 'admin' | 'user';
} }

View File

@ -1,4 +1,4 @@
import { PartialType } from '@nestjs/mapped-types'; import { PartialType } from '@nestjs/mapped-types';
import { CreateParticipantDto } from './create-participant.dto'; import { CreateParticipantDto } from './create-participant.dto';
export class UpdateParticipantDto extends PartialType(CreateParticipantDto) {} export class UpdateParticipantDto extends PartialType(CreateParticipantDto) {}

View File

@ -1,40 +1,26 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Column, Entity, ManyToOne } from 'typeorm';
import { v4 as uuidv4 } from 'uuid';
import { BaseEntity } from 'src/_core/entity/_base.entity'; import { BaseEntity } from 'src/_core/entity/_base.entity';
import * as mongoosePaginate from 'mongoose-paginate-v2'; import { Form } from 'src/form/entity/form.entity';
import { Document } from 'mongoose';
@Schema({ _id: false, id: false }) // Disable _id and virtual id @Entity({ name: 'participants' })
export class Participant extends BaseEntity { export class Participant extends BaseEntity {
@Prop({ @Column({
type: String, type: 'uuid',
default: () => uuidv4(), unique: true,
required: true,
immutable: true,
}) })
userId: string; userId: string;
@Prop({ @Column({
type: String, type: 'enum',
enum: ['moderator', 'tester', 'admin', 'user'], enum: ['moderator', 'tester', 'admin', 'user'],
default: 'user', default: 'user',
required: true,
}) })
role: 'moderator' | 'tester' | 'admin' | 'user'; role: 'moderator' | 'tester' | 'admin' | 'user';
@Prop({ @Column()
type: String,
required: true,
})
displayName: string; displayName: string;
}
export type ParticipantDocument = Participant & Document; // Many-to-One relationship with Form
export const ParticipantSchema = SchemaFactory.createForClass(Participant); @ManyToOne(() => Form, form => form.participants)
ParticipantSchema.plugin(mongoosePaginate); form: Form;
ParticipantSchema.set('toJSON', { }
transform: (doc: ParticipantDocument, ret: Participant & { _id?: any }) => {
delete ret._id;
return ret;
},
});

View File

@ -1,4 +1,4 @@
import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe } from '@nestjs/common'; import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query } from '@nestjs/common';
import { ParticipantService } from './participant.service'; import { ParticipantService } from './participant.service';
import { CreateParticipantDto } from './dto/create-participant.dto'; import { CreateParticipantDto } from './dto/create-participant.dto';
import { UpdateParticipantDto } from './dto/update-participant.dto'; import { UpdateParticipantDto } from './dto/update-participant.dto';
@ -14,7 +14,7 @@ export class ParticipantController {
return this.participantService.create(body); return this.participantService.create(body);
} }
@Get('findAll') // returns non uuid id !!! @Get('findAll')
async findAll( async findAll(
@Query('page') page: string = '1', @Query('page') page: string = '1',
@Query('limit') limit: string = '10', @Query('limit') limit: string = '10',
@ -32,22 +32,19 @@ export class ParticipantController {
return this.participantService.findAll(parseInt(page), parseInt(limit)); return this.participantService.findAll(parseInt(page), parseInt(limit));
} }
@Get(':id') @Get(':userId')
async findOne(@Param('id', new ParseUUIDPipe()) id: string): Promise<Participant | null> { async findOne(@Param('userId') userId: string): Promise<Participant | null> {
return this.participantService.findById(id); return this.participantService.findByUserId(userId);
} }
@Patch(':id') @Patch(':userId')
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
async update( async update(@Param('userId') userId: string, @Body() body: UpdateParticipantDto): Promise<Participant | null> {
@Param('id', new ParseUUIDPipe()) id: string, return this.participantService.update(userId, body);
@Body() body: UpdateParticipantDto,
): Promise<Participant | null> {
return this.participantService.update(id, body);
} }
@Delete(':id') @Delete(':userId')
async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> { async remove(@Param('userId') userId: string): Promise<void> {
return this.participantService.remove(id); return this.participantService.remove(userId);
} }
} }

View File

@ -1,12 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; import { TypeOrmModule } from '@nestjs/typeorm';
import { ParticipantService } from './participant.service'; import { ParticipantService } from './participant.service';
import { ParticipantController } from './participant.controller'; import { ParticipantController } from './participant.controller';
import { Participant, ParticipantSchema } from './entity/participant.entity'; import { Participant } from './entity/participant.entity';
@Module({ @Module({
imports: [ imports: [
MongooseModule.forFeature([{ name: Participant.name, schema: ParticipantSchema }]), TypeOrmModule.forFeature([Participant]),
], ],
controllers: [ParticipantController], controllers: [ParticipantController],
providers: [ParticipantService], providers: [ParticipantService],

View File

@ -1,45 +1,31 @@
import { Injectable } from '@nestjs/common'; import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectRepository } from '@nestjs/typeorm';
import { Model } from 'mongoose'; import { Repository } from 'typeorm';
import { CreateParticipantDto } from './dto/create-participant.dto'; import { CreateParticipantDto } from './dto/create-participant.dto';
import { Participant, ParticipantDocument } from './entity/participant.entity'; import { Participant } from './entity/participant.entity';
import { UpdateParticipantDto } from './dto/update-participant.dto'; import { UpdateParticipantDto } from './dto/update-participant.dto';
import { v4 as uuidv4 } from 'uuid';
export 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 ParticipantService { export class ParticipantService {
constructor( constructor(
@InjectModel(Participant.name) @InjectRepository(Participant)
private readonly participantModel: PaginateModel<ParticipantDocument>, private readonly participantRepository: Repository<Participant>,
) {} ) {}
async create(data: CreateParticipantDto): Promise<Participant> { async create(data: CreateParticipantDto): Promise<Participant> {
const participant = new this.participantModel({ try {
...data, const participant = this.participantRepository.create({
id: data.id || undefined, // Let BaseEntity generate UUID ...data,
metadata: data.metadata || { entries: [] }, metadata: data.metadata || { entries: [] },
}); status: data.status || 'active',
return participant.save(); });
return await this.participantRepository.save(participant);
} catch (error) {
throw new InternalServerErrorException(`Failed to create participant: ${error.message}`);
}
} }
async findAll(page = 1, limit = 10): Promise<{ // returns non uuid id !!! async findAll(page = 1, limit = 10): Promise<{
docs: Participant[]; docs: Participant[];
totalDocs: number; totalDocs: number;
limit: number; limit: number;
@ -50,26 +36,51 @@ export class ParticipantService {
nextPage: number | null; nextPage: number | null;
prevPage: number | null; prevPage: number | null;
}> { }> {
return this.participantModel.paginate( try {
{}, const skip = (page - 1) * limit;
{ page, limit, lean: true}, const [docs, totalDocs] = await this.participantRepository.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 InternalServerErrorException(`Failed to fetch participants: ${error.message}`);
}
} }
async findById(id: string): Promise<Participant | null> { async findByUserId(userId: string): Promise<Participant | null> {
return this.participantModel.findOne({ id }).lean().exec(); try {
return await this.participantRepository.findOne({ where: { userId } });
} catch (error) {
throw new InternalServerErrorException(`Failed to find participant: ${error.message}`);
}
} }
async update(id: string, data: UpdateParticipantDto): Promise<Participant | null> { async update(userId: string, data: UpdateParticipantDto): Promise<Participant | null> {
return this.participantModel try {
.findOneAndUpdate( await this.participantRepository.update({ userId }, { ...data, updatedAt: new Date() });
{ id }, return await this.participantRepository.findOne({ where: { userId } });
{ $set: { ...data, updatedAt: new Date() } }, } catch (error) {
{ new: true }, throw new InternalServerErrorException(`Failed to update participant: ${error.message}`);
).lean().exec(); }
} }
async remove(id: string): Promise<void> { async remove(userId: string): Promise<void> {
await this.participantModel.deleteOne({ id }).exec(); try {
await this.participantRepository.delete({ userId });
} catch (error) {
throw new InternalServerErrorException(`Failed to delete participant: ${error.message}`);
}
} }
} }

View File

@ -9,4 +9,4 @@ export class CreateParticipantGroupDto extends BaseDto{
@IsEnum(['confidential','delphi','one-time-question','one-time-section','one-time-page','one-time-form']) @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'; type: 'confidential'|'delphi'|'one-time-question'|'one-time-section'|'one-time-page'|'one-time-form';
} }

View File

@ -1,31 +1,20 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import {
import { BaseEntity } from 'src/_core/entity/_base.entity'; Entity,
import * as mongoosePaginate from 'mongoose-paginate-v2'; PrimaryGeneratedColumn,
import { Document } from 'mongoose'; Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { BaseEntity } from '../../_core/entity/_base.entity';
@Schema({ _id: false, id: false }) // Disable _id and virtual id @Entity()
export class ParticipantGroup extends BaseEntity { export class ParticipantGroup extends BaseEntity {
@Prop({ @Column()
type: String,
required: true,
})
title: string; title: string;
@Prop({ @Column({
type: String, type: 'enum',
enum: ['confidential','delphi','one-time-question','one-time-section','one-time-page','one-time-form'], 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'; 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

@ -1,14 +1,12 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; import { TypeOrmModule } from '@nestjs/typeorm';
import { ParticipantGroup } from './entity/participantGroup.entity';
import { ParticipantGroupService } from './participantGroup.service'; import { ParticipantGroupService } from './participantGroup.service';
import { ParticipantGroupController } from './participantGroup.controller'; import { ParticipantGroupController } from './participantGroup.controller';
import { ParticipantGroup, ParticipantGroupSchema } from './entity/participantGroup.entity';
@Module({ @Module({
imports: [ imports: [TypeOrmModule.forFeature([ParticipantGroup])],
MongooseModule.forFeature([{ name: ParticipantGroup.name, schema: ParticipantGroupSchema }]),
],
controllers: [ParticipantGroupController], controllers: [ParticipantGroupController],
providers: [ParticipantGroupService], providers: [ParticipantGroupService],
}) })
export class ParticipanGrouptModule {} export class ParticipanGrouptModule {}

View File

@ -1,60 +1,64 @@
import { Injectable } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectRepository } from '@nestjs/typeorm';
import { Model } from 'mongoose'; import { Repository } from 'typeorm';
import { ParticipantGroup } from './entity/participantGroup.entity';
import { CreateParticipantGroupDto } from './dto/create-participantGroup.dto'; import { CreateParticipantGroupDto } from './dto/create-participantGroup.dto';
import { ParticipantGroup, ParticipantGroupDocument } from './entity/participantGroup.entity';
import { UpdateParticipantGroupDto } from './dto/update-participantGroup.dto'; import { UpdateParticipantGroupDto } from './dto/update-participantGroup.dto';
import { v4 as uuidv4 } from 'uuid';
import { PaginateModel } from '../participant/participant.service';
@Injectable() @Injectable()
export class ParticipantGroupService { export class ParticipantGroupService {
constructor( constructor(
@InjectModel(ParticipantGroup.name) @InjectRepository(ParticipantGroup)
private readonly participantGroupModel: PaginateModel<ParticipantGroupDocument>, private readonly participantGroupRepo: Repository<ParticipantGroup>,
) {} ) {}
async create(data: CreateParticipantGroupDto): Promise<ParticipantGroup> { async create(data: CreateParticipantGroupDto): Promise<ParticipantGroup> {
const participantGroup = new this.participantGroupModel({ const group = this.participantGroupRepo.create({
...data, ...data,
id: data.id || undefined, // Let BaseEntity generate UUID
metadata: data.metadata || { entries: [] },
}); });
return participantGroup.save(); return await this.participantGroupRepo.save(group);
} }
async findAll(page = 1, limit = 10): Promise<{ // returns non uuid id !!! async findAll(page = 1, limit = 10) {
docs: ParticipantGroup[]; const [docs, totalDocs] = await this.participantGroupRepo.findAndCount({
totalDocs: number; skip: (page - 1) * limit,
limit: number; take: limit,
page: number; });
totalPages: number;
hasNextPage: boolean; const totalPages = Math.ceil(totalDocs / limit);
hasPrevPage: boolean; return {
nextPage: number | null; docs,
prevPage: number | null; totalDocs,
}> { limit,
return this.participantGroupModel.paginate( page,
{}, totalPages,
{ page, limit, lean: true }, hasNextPage: page < totalPages,
); hasPrevPage: page > 1,
nextPage: page < totalPages ? page + 1 : null,
prevPage: page > 1 ? page - 1 : null,
};
} }
async findById(id: string): Promise<ParticipantGroup | null> { async findById(id: string): Promise<ParticipantGroup> {
return this.participantGroupModel.findOne({ id }).lean().exec(); const group = await this.participantGroupRepo.findOne({ where: { id } });
if (!group) {
throw new NotFoundException(`Participant group with ID "${id}" not found`);
}
return group;
} }
async update(id: string, data: UpdateParticipantGroupDto): Promise<ParticipantGroup | null> { async update(id: string, data: UpdateParticipantGroupDto): Promise<ParticipantGroup> {
return this.participantGroupModel const result = await this.participantGroupRepo.update({ id }, { ...data, updatedAt: new Date() });
.findOneAndUpdate( if (result.affected === 0) {
{ id }, throw new NotFoundException(`Participant group with ID "${id}" not found`);
{ $set: { ...data, updatedAt: new Date() } }, }
{ new: true }, return await this.findById(id);
).lean().exec();
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
await this.participantGroupModel.deleteOne({ id }).exec(); const result = await this.participantGroupRepo.delete({ id });
if (result.affected === 0) {
throw new NotFoundException(`Participant group with ID "${id}" not found`);
}
} }
} }

View File

@ -1,16 +1,22 @@
import { IsString, IsBoolean, IsOptional, IsUUID, ValidateNested, IsArray, IsEnum, IsUrl,} from 'class-validator'; import {
IsString,
IsBoolean,
IsOptional,
IsArray,
IsEnum,
ValidateNested,
} from 'class-validator';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import {CreateAttachmentDto} from '../../attachment/dto/create_attachment.dto';
import { BaseDto } from '../../_core/dto/base.dto'; import { BaseDto } from '../../_core/dto/base.dto';
import { CreateOptionDto } from '../../option/dto/create-option.dto'; import { CreateOptionDto } from '../../option/dto/create-option.dto';
import { CreateAttachmentDto } from '../../attachment/dto/create_attachment.dto';
// Main DTO for creating a Question export class CreateQuestionDto extends BaseDto {
export class CreateQuestionDto extends BaseDto{
@IsString() @IsString()
text: string; text: string;
@IsEnum(['multiple-choice', 'single-choice', 'text', 'rating', 'file']) @IsEnum(['multiple-choice', 'single-choice', 'text'])
type: string; type: 'multiple-choice' | 'single-choice' | 'text';
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()

View File

@ -1,67 +1,49 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
import { Document } from 'mongoose';
import { BaseEntity } from 'src/_core/entity/_base.entity'; import { BaseEntity } from 'src/_core/entity/_base.entity';
import * as mongoosePaginate from 'mongoose-paginate-v2'; import { Attachment } from '../../attachment/entity/attachment.entity';
import { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity'; import { Option } from '../../option/entity/option.entity';
import { Option, OptionSchema } from '../../option/entity/option.entity'; import { Answer } from '../../answer/entity/answer.entity';
import { FormSection } from '../../formSection/entity/formSection.entity';
@Schema({ _id: false, id: false }) // Disable _id and virtual id @Entity({ name: 'questions' })
export class Question extends BaseEntity { export class Question extends BaseEntity {
@Prop({ @Column()
type: String,
required: true,
})
text: string; text: string;
@Prop({ @Column({
type: String, type: 'enum',
required: true,
enum: ['multiple-choice', 'single-choice', 'text'], enum: ['multiple-choice', 'single-choice', 'text'],
default: 'text', default: 'text',
}) })
type: 'multiple-choice' | 'single-choice' | 'text'; type: 'multiple-choice' | 'single-choice' | 'text';
@Prop({ @Column({
type: Boolean, type: 'boolean',
default: false, default: false,
required: true,
}) })
isRequired: boolean; isRequired: boolean;
@Prop({ @Column({
type: Boolean, type: 'boolean',
default: false, default: false,
required: true,
}) })
isConfidential: boolean; // no real-time updates for normal users isConfidential: boolean;
@Prop({ @Column({
type: String, type: 'varchar',
required: false, // Assuming validation rules might not always be present nullable: true,
}) })
validationRules?: string; validationRules?: string;
@Prop({ @OneToMany(() => Option, (option) => option.question, { cascade: true })
type: [OptionSchema],
default: [],
})
options: Option[]; options: Option[];
@Prop({ @OneToMany(() => Attachment, (attachment) => attachment.question, { cascade: true })
type: [AttachmentSchema],
default: [],
})
attachments: Attachment[]; attachments: Attachment[];
}
export type QuestionDocument = Question & Document; @OneToMany(() => Answer, answer => answer.question, { cascade: true })
export const QuestionSchema = SchemaFactory.createForClass(Question); answers: Answer[]; // Answers to this question
QuestionSchema.plugin(mongoosePaginate);
// Transform the output to remove the internal '_id' @ManyToOne(() => FormSection, formSection => formSection.questions)
QuestionSchema.set('toJSON', { formSection: FormSection;
transform: (doc: QuestionDocument, ret: Question & { _id?: any }) => { }
delete ret._id;
return ret;
},
});

View File

@ -1,15 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose'; import { TypeOrmModule } from '@nestjs/typeorm';
import { QuestionService } from './question.service'; import { Question } from './entity/question.entity';
import { QuestionController } from './question.controller'; import { QuestionController } from './question.controller';
import { Question, QuestionSchema } from './entity/question.entity'; import { QuestionService } from './question.service';
@Module({ @Module({
imports: [ imports: [TypeOrmModule.forFeature([Question])],
MongooseModule.forFeature([
{ name: Question.name, schema: QuestionSchema },
]),
],
controllers: [QuestionController], controllers: [QuestionController],
providers: [QuestionService], providers: [QuestionService],
}) })

View File

@ -1,81 +1,58 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose'; import { InjectRepository } from '@nestjs/typeorm';
import { Model } from 'mongoose'; import { Repository } from 'typeorm';
import { Question, QuestionDocument } from './entity/question.entity'; import { Question } 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';
@Injectable() @Injectable()
export class QuestionService { export class QuestionService {
constructor( constructor(
@InjectModel(Question.name) @InjectRepository(Question)
private readonly questionModel: PaginateModel<QuestionDocument>, private readonly questionRepo: Repository<Question>,
) {} ) {}
async create(data: CreateQuestionDto): Promise<Question> { async create(data: CreateQuestionDto): Promise<Question> {
const question = new this.questionModel({ const question = this.questionRepo.create(data);
...data, return this.questionRepo.save(question);
id: data.id || undefined, // Let BaseEntity generate UUID if not provided
metadata: data.metadata || { entries: [] },
});
return question.save();
} }
async findAll( async findAll(page = 1, limit = 10) {
page = 1, const [docs, totalDocs] = await this.questionRepo.findAndCount({
limit = 10, skip: (page - 1) * limit,
): Promise<{ take: limit,
docs: Question[]; });
totalDocs: number;
limit: number; const totalPages = Math.ceil(totalDocs / limit);
page: number; return {
totalPages: number; docs,
hasNextPage: boolean; totalDocs,
hasPrevPage: boolean; limit,
nextPage: number | null; page,
prevPage: number | null; totalPages,
}> { hasNextPage: page < totalPages,
// Selects all fields from the Question entity for the response hasPrevPage: page > 1,
return this.questionModel.paginate( nextPage: page < totalPages ? page + 1 : null,
{}, prevPage: page > 1 ? page - 1 : null,
{ page, limit, lean: true }, };
);
} }
async findById(id: string): Promise<Question | null> { async findById(id: string): Promise<Question | null> {
const question = await this.questionModel const question = await this.questionRepo.findOneBy({ id });
.findOne({ id })
.lean()
.exec();
if (!question) { if (!question) {
throw new NotFoundException(`Question with ID "${id}" not found`); throw new NotFoundException(`Question with ID "${id}" not found`);
} }
return question; return question;
} }
async update( async update(id: string, data: UpdateQuestionDto): Promise<Question | null> {
id: string, await this.questionRepo.update(id, data);
data: UpdateQuestionDto, return this.findById(id);
): Promise<Question | null> {
const updatedQuestion = await this.questionModel
.findOneAndUpdate(
{ id },
{ $set: { ...data, updatedAt: new Date() } },
{ new: true },
)
.lean()
.exec();
if (!updatedQuestion) {
throw new NotFoundException(`Question with ID "${id}" not found`);
}
return updatedQuestion;
} }
async remove(id: string): Promise<void> { async remove(id: string): Promise<void> {
const result = await this.questionModel.deleteOne({ id }).exec(); const result = await this.questionRepo.delete(id);
if (result.deletedCount === 0) { if (result.affected === 0) {
throw new NotFoundException(`Question with ID "${id}" not found`); throw new NotFoundException(`Question with ID "${id}" not found`);
} }
} }