diff --git a/package-lock.json b/package-lock.json index e0854a6..2380f03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,8 @@ "@nestjs/typeorm": "^11.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "glob": "^11.0.3", + "jsonwebtoken": "^9.0.2", "libphonenumber-js": "^1.12.9", "mongodb": "^6.17.0", "mongoose": "^8.16.3", @@ -38,6 +40,7 @@ "@swc/core": "^1.12.11", "@types/express": "^5.0.3", "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.0.13", "@types/supertest": "^6.0.3", "eslint": "^9.31.0", @@ -1381,7 +1384,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, "engines": { "node": "20 || >=22" } @@ -1390,7 +1392,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", - "dev": true, "dependencies": { "@isaacs/balanced-match": "^4.0.1" }, @@ -2454,6 +2455,29 @@ "node": ">=4.0" } }, + "node_modules/@nestjs/cli/node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@nestjs/cli/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -2481,6 +2505,21 @@ "node": ">= 0.6" } }, + "node_modules/@nestjs/cli/node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "dev": true, + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@nestjs/cli/node_modules/schema-utils": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", @@ -3517,6 +3556,16 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -3529,6 +3578,12 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true + }, "node_modules/@types/node": { "version": "24.0.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", @@ -5435,6 +5490,11 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6194,6 +6254,14 @@ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -7372,14 +7440,13 @@ } }, "node_modules/glob": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", - "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", - "dev": true, + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^4.0.1", - "minimatch": "^10.0.0", + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -7416,7 +7483,6 @@ "version": "10.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "dev": true, "dependencies": { "@isaacs/brace-expansion": "^5.0.0" }, @@ -7942,7 +8008,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -8807,6 +8872,46 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kareem": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz", @@ -8959,6 +9064,36 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -8971,6 +9106,11 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -9791,7 +9931,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" @@ -9807,7 +9946,6 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "dev": true, "engines": { "node": "20 || >=22" } diff --git a/package.json b/package.json index f040381..38ada62 100644 --- a/package.json +++ b/package.json @@ -27,14 +27,16 @@ "@nestjs/mongoose": "^11.0.3", "@nestjs/platform-express": "^11.1.3", "@nestjs/typeorm": "^11.0.0", - "pg": "^8.11.3", "class-transformer": "^0.5.1", "class-validator": "^0.14.2", + "glob": "^11.0.3", + "jsonwebtoken": "^9.0.2", "libphonenumber-js": "^1.12.9", "mongodb": "^6.17.0", "mongoose": "^8.16.3", "mongoose-paginate-v2": "^1.9.1", "nestjs-paginate": "^12.5.1", + "pg": "^8.11.3", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.2", "uuid": "^11.1.0" @@ -49,6 +51,7 @@ "@swc/core": "^1.12.11", "@types/express": "^5.0.3", "@types/jest": "^30.0.0", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.0.13", "@types/supertest": "^6.0.3", "eslint": "^9.31.0", diff --git a/src/_core/entity/_base.entity.ts b/src/_core/entity/_base.entity.ts index e45888b..0b5104b 100644 --- a/src/_core/entity/_base.entity.ts +++ b/src/_core/entity/_base.entity.ts @@ -13,7 +13,7 @@ export abstract class BaseEntity { updatedAt: Date; @Column(() => Metadata) - metadata: Metadata; + metadata: Metadata = new Metadata(); @Column({ type: 'enum', diff --git a/src/_core/entity/_metadata.entity.ts b/src/_core/entity/_metadata.entity.ts index 4921697..8fcf160 100644 --- a/src/_core/entity/_metadata.entity.ts +++ b/src/_core/entity/_metadata.entity.ts @@ -7,11 +7,11 @@ export class Metadata { primary: true, default: () => 'uuid_generate_v4()', }) - id: string; + id?: string; @Column({ type: 'jsonb', default: () => "'[]'", }) - entries: { key: string; value: string }[]; + entries?: { key: string; value: string }[]; } \ No newline at end of file diff --git a/src/app.controller.ts b/src/app.controller.ts index cce879e..1b72847 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,12 +1,24 @@ import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; - +import { InjectDataSource } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; @Controller() export class AppController { - constructor(private readonly appService: AppService) {} - + constructor(private readonly appService: AppService, + @InjectDataSource() private readonly dataSource: DataSource) {} @Get() getHello(): string { return this.appService.getHello(); } + + @Get('test-db') + async testDatabaseConnection() { + try { + await this.dataSource.query('SELECT 1'); + return { message: 'Database connection successful' }; + } catch (error) { + return { message: 'Database connection failed', error: error.message }; + } + } + } diff --git a/src/config/database.config.ts b/src/config/database.config.ts index 0449181..a348458 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -1,13 +1,39 @@ +// src/config/database.config.ts import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { Participant } from '../participant/entity/participant.entity'; +import { Metadata } from '../_core/entity/_metadata.entity'; +import { Form } from '../form/entity/form.entity'; +import { Realm } from '../realm/entity/realm.entity'; +import { Question } from '../question/entity/question.entity'; +import { Answer } from '../answer/entity/answer.entity'; +import { Option } from '../option/entity/option.entity'; +import { Attachment } from '../attachment/entity/attachment.entity'; +import { FormSection } from '../formSection/entity/formSection.entity'; +import { FormPage } from '../formPage/entity/formPage.entity'; +import { FormResult } from '../formResult/entity/formResult.entity'; +import { ParticipantGroup } from '../participantGroup/entity/participantGroup.entity'; export const typeOrmConfig: TypeOrmModuleOptions = { type: 'postgres', host: 'localhost', - port: 5432, - username: 'your_username', - password: 'your_password', - database: 'your_database', - entities: [__dirname + '/**/*.entity{.ts,.js}'], + port: 5433, + username: 'postgres', + password: '1111', + database: 'postgres', + entities: [ + Participant, + Metadata, + Form, + Realm, + Question, + Answer, + Option, + Attachment, + FormSection, + FormPage, + FormResult, + ParticipantGroup, + ], synchronize: true, - extensions: ['uuid-ossp'], + logging: true, }; \ No newline at end of file diff --git a/src/form/entity/form.entity.ts b/src/form/entity/form.entity.ts index 8a1fb30..4eca37f 100644 --- a/src/form/entity/form.entity.ts +++ b/src/form/entity/form.entity.ts @@ -1,7 +1,8 @@ -import { Entity, Column, OneToMany } from 'typeorm'; +import { Entity, Column, OneToMany, ManyToOne } from 'typeorm'; import { BaseEntity } from '../../_core/entity/_base.entity'; import { FormPage } from '../../formPage/entity/formPage.entity'; -import { Participant } from '../../participant/entity/participant.entity'; // Assuming Participant entity exists +import { Participant } from '../../participant/entity/participant.entity'; +import { Realm } from '../../realm/entity/realm.entity'; @Entity() export class Form extends BaseEntity { @@ -19,6 +20,10 @@ export class Form extends BaseEntity { @OneToMany(() => Participant, participant => participant.form, { cascade: true, eager: true }) participants: Participant[]; + // Many-to-One relationship with Realm (ADD THIS) + @ManyToOne(() => Realm, realm => realm.forms) + realm: Realm; + // 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. @@ -28,8 +33,6 @@ export class Form extends BaseEntity { // If you need attachments directly on Form, please provide the Attachment entity structure. - - // @Prop({ // type: [AttachmentSchema], // default: [], diff --git a/src/formResult/dto/create-formResult.dto.ts b/src/formResult/dto/create-formResult.dto.ts index b5b0263..ab3f9f1 100644 --- a/src/formResult/dto/create-formResult.dto.ts +++ b/src/formResult/dto/create-formResult.dto.ts @@ -2,7 +2,7 @@ import { IsNumber, IsString, IsOptional, IsUUID, ValidateNested, IsArray, IsDate import { Type } from 'class-transformer'; import { BaseDto } from '../../_core/dto/base.dto'; -// DTO for Options +// DTO for Options (remains the same as it's validation-focused) class CreateOptionsDto { @IsString() value: string; @@ -11,7 +11,7 @@ class CreateOptionsDto { count: number; } -// DTO for Opinion +// DTO for Opinion (remains the same) class CreateOpinionDto { @ValidateNested() @Type(() => CreateOptionsDto) @@ -21,7 +21,7 @@ class CreateOpinionDto { questionId: string; } -// Main DTO for creating a FormResult +// Main DTO for creating a FormResult (remains largely the same) export class CreateFormResultDto extends BaseDto { @IsNumber() numParticipants: number; @@ -39,4 +39,4 @@ export class CreateFormResultDto extends BaseDto { @ValidateNested({ each: true }) @Type(() => CreateOpinionDto) opinions: CreateOpinionDto[]; -} \ No newline at end of file +} diff --git a/src/formResult/entity/formResult.entity.ts b/src/formResult/entity/formResult.entity.ts index f0385e7..6233f7d 100644 --- a/src/formResult/entity/formResult.entity.ts +++ b/src/formResult/entity/formResult.entity.ts @@ -1,98 +1,41 @@ -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document } from 'mongoose'; -import { BaseEntity } from 'src/_core/entity/_base.entity'; -import * as mongoosePaginate from 'mongoose-paginate-v2'; -import { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity'; -import { Option, OptionSchema } from '../../option/entity/option.entity'; -import { UUID } from 'mongodb'; +import { Entity, Column } from 'typeorm'; +import { BaseEntity } from '../../_core/entity/_base.entity'; -// Sub-schema for Options -@Schema({ _id: false, id: false }) -class Options { - @Prop({ - type: String, - required: true, - }) +// Options class (embedded) +export class Options { + @Column() value: string; - @Prop({ - type: Number, - required: true, - min: 0, - default: 0 - }) + @Column({ type: 'int', default: 0 }) count: number; } -export const OptionsSchema = SchemaFactory.createForClass(Options); - -// Sub-schema for Opinion -@Schema({ _id: false, id: false }) -class Opinion { - @Prop({ - type: OptionsSchema, - required: true, - }) +// Opinion class (embedded) +export class Opinion { + @Column(() => Options) options: Options; - @Prop({ - type: UUID, - required: true, + @Column({ + type: 'uuid', + unique: true, }) questionId: string; } -export const OpinionSchema = SchemaFactory.createForClass(Opinion); - -@Schema({ _id: false, id: false }) // Disable _id and virtual id +@Entity() export class FormResult extends BaseEntity { - @Prop({ - type: Number, - required: true, - min: 0, - default: 0, - }) - numParticipants: string; + @Column({ type: 'int', default: 0 }) + numParticipants: number; - @Prop({ - type: Number, - required: true, - min: 0, - default: 0, - }) - numQuestions: string; + @Column({ type: 'int', default: 0 }) + numQuestions: number; - @Prop({ - type: Number, - required: true, - min: 0, - default: 0, - }) - numAnswers: string; + @Column({ type: 'int', default: 0 }) + numAnswers: number; - @Prop({ - type: Number, - required: true, - min: 0, - default: 0, - }) - numComplete: string; // number of people who completed this form + @Column({ type: 'int', default: 0 }) + numComplete: number; - @Prop({ - type: [OpinionSchema], - default: [], - }) + @Column(() => Opinion) opinions: Opinion[]; } - -export type FormResultDocument = FormResult & Document; -export const FormResultSchema = SchemaFactory.createForClass(FormResult); -FormResultSchema.plugin(mongoosePaginate); - -// Transform the output to remove the internal '_id' -FormResultSchema.set('toJSON', { - transform: (doc: FormResultDocument, ret: FormResult & { _id?: any }) => { - delete ret._id; - return ret; - }, -}); diff --git a/src/formResult/formResult.controller.ts b/src/formResult/formResult.controller.ts index 359b7d3..71b5247 100644 --- a/src/formResult/formResult.controller.ts +++ b/src/formResult/formResult.controller.ts @@ -19,7 +19,6 @@ export class FormResultController { @Query('page') page: string = '1', @Query('limit') limit: string = '10', ) { - // The service returns the full pagination object return this.formResultService.findAll(parseInt(page, 10), parseInt(limit, 10)); } @@ -44,4 +43,4 @@ export class FormResultController { async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise { return this.formResultService.remove(id); } -} +} \ No newline at end of file diff --git a/src/formResult/formResult.module.ts b/src/formResult/formResult.module.ts index 0f12280..c9d9a29 100644 --- a/src/formResult/formResult.module.ts +++ b/src/formResult/formResult.module.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { FormResultService } from './formResult.service'; import { FormResultController } from './formResult.controller'; -import { FormResult, FormResultSchema } from './entity/formResult.entity'; +import { FormResult } from './entity/formResult.entity'; @Module({ imports: [ - MongooseModule.forFeature([ - { name: FormResult.name, schema: FormResultSchema }, + TypeOrmModule.forFeature([ + FormResult, ]), ], controllers: [FormResultController], diff --git a/src/formResult/formResult.service.ts b/src/formResult/formResult.service.ts index c0a98ad..374cdf9 100644 --- a/src/formResult/formResult.service.ts +++ b/src/formResult/formResult.service.ts @@ -1,25 +1,22 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; -import { FormResult, FormResultDocument } from './entity/formResult.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { FormResult } from './entity/formResult.entity'; import { CreateFormResultDto } from './dto/create-formResult.dto'; import { UpdateFormResultDto } from './dto/update-formResult.dto'; -import { PaginateModel } from '../participant/participant.service'; @Injectable() export class FormResultService { constructor( - @InjectModel(FormResult.name) - private readonly formResultModel: PaginateModel, + @InjectRepository(FormResult) + private readonly formResultRepo: Repository, ) {} async create(data: CreateFormResultDto): Promise { - const formResult = new this.formResultModel({ + const formResult = this.formResultRepo.create({ ...data, - id: data.id || undefined, // Let BaseEntity generate UUID if not provided - metadata: data.metadata || { entries: [] }, }); - return formResult.save(); + return await this.formResultRepo.save(formResult); } async findAll( @@ -36,18 +33,31 @@ export class FormResultService { nextPage: number | null; prevPage: number | null; }> { - // Selects all fields from the FormResult entity for the response - return this.formResultModel.paginate( - {}, - { page, limit, lean: true }, - ); + const [docs, totalDocs] = await this.formResultRepo.findAndCount({ + skip: (page - 1) * limit, + take: limit, + // No specific relations needed here unless FormResult links to other entities + }); + + const totalPages = Math.ceil(totalDocs / limit); + return { + docs, + totalDocs, + limit, + page, + totalPages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1, + nextPage: page < totalPages ? page + 1 : null, + prevPage: page > 1 ? page - 1 : null, + }; } async findById(id: string): Promise { - const formResult = await this.formResultModel - .findOne({ id }) - .lean() - .exec(); + const formResult = await this.formResultRepo.findOne({ + where: { id }, + // No specific relations needed here unless FormResult links to other entities + }); if (!formResult) { throw new NotFoundException(`FormResult with ID "${id}" not found`); } @@ -58,24 +68,20 @@ export class FormResultService { id: string, data: UpdateFormResultDto, ): Promise { - const updatedFormResult = await this.formResultModel - .findOneAndUpdate( - { id }, - { $set: { ...data, updatedAt: new Date() } }, - { new: true }, - ) - .lean() - .exec(); + const result = await this.formResultRepo.update( + { id }, + { ...data, updatedAt: new Date() }, + ); - if (!updatedFormResult) { + if (result.affected === 0) { throw new NotFoundException(`FormResult with ID "${id}" not found`); } - return updatedFormResult; + return await this.findById(id); } async remove(id: string): Promise { - const result = await this.formResultModel.deleteOne({ id }).exec(); - if (result.deletedCount === 0) { + const result = await this.formResultRepo.delete({ id }); + if (result.affected === 0) { throw new NotFoundException(`FormResult with ID "${id}" not found`); } } diff --git a/src/participant/entity/participant.entity.ts b/src/participant/entity/participant.entity.ts index 410d9d8..b5245a7 100644 --- a/src/participant/entity/participant.entity.ts +++ b/src/participant/entity/participant.entity.ts @@ -1,6 +1,7 @@ import { Column, Entity, ManyToOne } from 'typeorm'; import { BaseEntity } from 'src/_core/entity/_base.entity'; import { Form } from 'src/form/entity/form.entity'; +import { Realm } from '../../realm/entity/realm.entity'; @Entity({ name: 'participants' }) export class Participant extends BaseEntity { @@ -23,4 +24,8 @@ export class Participant extends BaseEntity { // Many-to-One relationship with Form @ManyToOne(() => Form, form => form.participants) form: Form; + + // Many-to-One relationship with Realm (if a participant belongs to a realm) + @ManyToOne(() => Realm, realm => realm.participants) + realm: Realm; } \ No newline at end of file diff --git a/src/participant/participant.service.ts b/src/participant/participant.service.ts index 047a261..fa7f5ea 100644 --- a/src/participant/participant.service.ts +++ b/src/participant/participant.service.ts @@ -4,6 +4,8 @@ import { Repository } from 'typeorm'; import { CreateParticipantDto } from './dto/create-participant.dto'; import { Participant } from './entity/participant.entity'; import { UpdateParticipantDto } from './dto/update-participant.dto'; +import axios from 'axios'; +import { decode } from 'jsonwebtoken'; @Injectable() export class ParticipantService { @@ -14,11 +16,42 @@ export class ParticipantService { async create(data: CreateParticipantDto): Promise { try { + // Make the authentication request to get the token + const authResponse = await axios.post( + 'https://auth.didvan.com/realms/didvan/protocol/openid-connect/token', + new URLSearchParams({ + client_id: 'didvan-app', + username: 'alice', + password: 'developer_password', + grant_type: 'password', + }), + { + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + }, + ); + + // Decode the access token to extract the 'sub' claim + const token = authResponse.data.access_token; + const decodedToken: any = decode(token); + if (!decodedToken || !decodedToken.sub) { + throw new InternalServerErrorException('Failed to decode token or extract sub'); + } + + // Create the participant with the 'sub' as the ID and userId const participant = this.participantRepository.create({ ...data, - metadata: data.metadata || { entries: [] }, + id: decodedToken.sub, // Set the ID from the token's sub + userId: decodedToken.sub, // Set userId to the same sub + metadata: data.metadata + ? { entries: data.metadata.entries } // Ensure metadata is properly formatted + : { entries: [] }, status: data.status || 'active', + // Note: form and realm are not set as they are optional (ManyToOne) }); + + // Save the participant to the database return await this.participantRepository.save(participant); } catch (error) { throw new InternalServerErrorException(`Failed to create participant: ${error.message}`); diff --git a/src/realm/dto/create-realm.dto.ts b/src/realm/dto/create-realm.dto.ts index be885c2..1f765d3 100644 --- a/src/realm/dto/create-realm.dto.ts +++ b/src/realm/dto/create-realm.dto.ts @@ -1,11 +1,8 @@ -import { IsString, IsOptional, IsUUID, ValidateNested, IsArray, IsEnum, IsDateString } from 'class-validator'; +import { IsString, IsOptional, ValidateNested, IsArray } from 'class-validator'; import { Type } from 'class-transformer'; import { BaseDto } from '../../_core/dto/base.dto'; -import { CreateAttachmentDto } from '../../attachment/dto/create_attachment.dto'; -import { CreateQuestionDto } from '../../question/dto/create-question.dto'; -import { CreateFormPageDto } from '../../formPage/dto/create-formPage.dto'; -import { CreateParticipantDto } from '../../participant/dto/create-participant.dto'; import { CreateFormDto } from '../../form/dto/create-form.dto'; +import { CreateParticipantDto } from '../../participant/dto/create-participant.dto'; export class CreateRealmDto extends BaseDto { @IsString() @@ -23,10 +20,9 @@ export class CreateRealmDto extends BaseDto { @IsArray() @ValidateNested({ each: true }) @Type(() => CreateParticipantDto) - participant: CreateParticipantDto[]; + participants: CreateParticipantDto[]; // Changed from 'participant' to 'participants' for consistency with array - @IsArray() - @ValidateNested({ each: true }) + @ValidateNested() // Not IsArray, as it's a single owner @Type(() => CreateParticipantDto) owner: CreateParticipantDto; -} \ No newline at end of file +} diff --git a/src/realm/entity/realm.entity.ts b/src/realm/entity/realm.entity.ts index 4e9bd32..b98365d 100644 --- a/src/realm/entity/realm.entity.ts +++ b/src/realm/entity/realm.entity.ts @@ -1,58 +1,27 @@ -import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; -import { Document } from 'mongoose'; -import { BaseEntity } from 'src/_core/entity/_base.entity'; -import * as mongoosePaginate from 'mongoose-paginate-v2'; -import { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity'; -import { Option, OptionSchema } from '../../option/entity/option.entity'; -import { Answer, AnswerSchema } from '../../answer/entity/answer.entity'; -import { Question, QuestionSchema } from '../../question/entity/question.entity'; -import { Form, FormSchema } from '../../form/entity/form.entity'; -import { Participant, ParticipantSchema } from '../../participant/entity/participant.entity'; +import { Entity, Column, OneToMany, OneToOne, JoinColumn } from 'typeorm'; +import { BaseEntity } from '../../_core/entity/_base.entity'; +import { Form } from '../../form/entity/form.entity'; +import { Participant } from '../../participant/entity/participant.entity'; -@Schema({ _id: false, id: false }) // Disable _id and virtual id +@Entity() export class Realm extends BaseEntity { - @Prop({ - type: String, - required: true, - }) + @Column() title: string; - @Prop({ - type: String, - required: true, - }) + @Column() description: string; - @Prop({ - type: [FormSchema], - default: [], - }) + // One-to-Many relationship with Form + @OneToMany(() => Form, form => form.realm, { cascade: true, eager: true }) forms: Form[]; - @Prop({ - type: [ParticipantSchema], - default: [], - }) + // One-to-Many relationship with Participant (for general participants in the realm) + @OneToMany(() => Participant, participant => participant.realm, { cascade: true, eager: true }) participants: Participant[]; - @Prop({ - type: ParticipantSchema, - }) + // One-to-One relationship with Participant (for the owner of the realm) + // Assuming 'owner' is a single Participant entity + @OneToOne(() => Participant, { cascade: true, eager: true }) + @JoinColumn() // This column will be added to the Realm table to store the owner's ID owner: Participant; - } - -export type RealmDocument = Realm & Document; -export const RealmSchema = SchemaFactory.createForClass(Realm); -RealmSchema.plugin(mongoosePaginate); - -// Transform the output to remove the internal '_id' -RealmSchema.set('toJSON', { - transform: (doc: RealmDocument, ret: Realm & { _id?: any }) => { - delete ret._id; - return ret; - }, -}); - - - diff --git a/src/realm/realm.controller.ts b/src/realm/realm.controller.ts index eefa58a..7d8d63d 100644 --- a/src/realm/realm.controller.ts +++ b/src/realm/realm.controller.ts @@ -19,7 +19,6 @@ export class RealmController { @Query('page') page: string = '1', @Query('limit') limit: string = '10', ) { - // The service returns the full pagination object return this.realmService.findAll(parseInt(page, 10), parseInt(limit, 10)); } diff --git a/src/realm/realm.module.ts b/src/realm/realm.module.ts index 2703a69..300371c 100644 --- a/src/realm/realm.module.ts +++ b/src/realm/realm.module.ts @@ -1,13 +1,29 @@ import { Module } from '@nestjs/common'; -import { MongooseModule } from '@nestjs/mongoose'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { RealmService } from './realm.service'; import { RealmController } from './realm.controller'; -import { Realm, RealmSchema } from './entity/realm.entity'; +import { Realm } from './entity/realm.entity'; +import { Form } from '../form/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'; @Module({ imports: [ - MongooseModule.forFeature([ - { name: Realm.name, schema: RealmSchema }, + TypeOrmModule.forFeature([ + Realm, + Form, + FormPage, + FormSection, + Attachment, + Question, + Answer, + Option, + Participant, ]), ], controllers: [RealmController], diff --git a/src/realm/realm.service.ts b/src/realm/realm.service.ts index e4500e3..01ffe05 100644 --- a/src/realm/realm.service.ts +++ b/src/realm/realm.service.ts @@ -1,25 +1,21 @@ +// src/realm/realm.service.ts import { Injectable, NotFoundException } from '@nestjs/common'; -import { InjectModel } from '@nestjs/mongoose'; -import { Model } from 'mongoose'; -import { Realm, RealmDocument } from './entity/realm.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Realm } from './entity/realm.entity'; import { CreateRealmDto } from './dto/create-realm.dto'; import { UpdateRealmDto } from './dto/update-realm.dto'; -import { PaginateModel } from '../participant/participant.service'; @Injectable() export class RealmService { constructor( - @InjectModel(Realm.name) - private readonly realmModel: PaginateModel, + @InjectRepository(Realm) + private readonly realmRepo: Repository, ) {} async create(data: CreateRealmDto): Promise { - const realm = new this.realmModel({ - ...data, - id: data.id || undefined, // Let BaseEntity generate UUID if not provided - metadata: data.metadata || { entries: [] }, - }); - return realm.save(); + const realm = this.realmRepo.create({ ...data }); + return await this.realmRepo.save(realm); } async findAll( @@ -36,18 +32,51 @@ export class RealmService { nextPage: number | null; prevPage: number | null; }> { - // Selects all fields from the Realm entity for the response - return this.realmModel.paginate( - {}, - { page, limit, lean: true }, - ); + const [docs, totalDocs] = await this.realmRepo.findAndCount({ + skip: (page - 1) * limit, + take: limit, + relations: [ + 'forms', + 'forms.pages', + 'forms.pages.formSections', + 'forms.pages.formSections.attachments', + 'forms.pages.formSections.questions', + 'forms.pages.formSections.questions.options', + 'forms.pages.formSections.questions.answers', + 'participants', + 'owner', + ], + }); + + const totalPages = Math.ceil(totalDocs / limit); + return { + docs, + totalDocs, + limit, + page, + totalPages, + hasNextPage: page < totalPages, + hasPrevPage: page > 1, + nextPage: page < totalPages ? page + 1 : null, + prevPage: page > 1 ? page - 1 : null, + }; } async findById(id: string): Promise { - const realm = await this.realmModel - .findOne({ id }) - .lean() - .exec(); + const realm = await this.realmRepo.findOne({ + where: { id }, + relations: [ + 'forms', + 'forms.pages', + 'forms.pages.formSections', + 'forms.pages.formSections.attachments', + 'forms.pages.formSections.questions', + 'forms.pages.formSections.questions.options', + 'forms.pages.formSections.questions.answers', + 'participants', + 'owner', + ], + }); if (!realm) { throw new NotFoundException(`Realm with ID "${id}" not found`); } @@ -58,24 +87,20 @@ export class RealmService { id: string, data: UpdateRealmDto, ): Promise { - const updatedRealm = await this.realmModel - .findOneAndUpdate( - { id }, - { $set: { ...data, updatedAt: new Date() } }, - { new: true }, - ) - .lean() - .exec(); + const result = await this.realmRepo.update( + { id }, + { ...data, updatedAt: new Date() }, + ); - if (!updatedRealm) { + if (result.affected === 0) { throw new NotFoundException(`Realm with ID "${id}" not found`); } - return updatedRealm; + return await this.findById(id); } async remove(id: string): Promise { - const result = await this.realmModel.deleteOne({ id }).exec(); - if (result.deletedCount === 0) { + const result = await this.realmRepo.delete({ id }); + if (result.affected === 0) { throw new NotFoundException(`Realm with ID "${id}" not found`); } }