Compare commits
No commits in common. "aeb7a73c4cfe98cb25a58bc5acb54690023467f4" and "50ead64e69f9634cdf53406bd294f5dfff781a13" have entirely different histories.
aeb7a73c4c
...
50ead64e69
|
|
@ -15,17 +15,13 @@
|
|||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/mongoose": "^11.0.3",
|
||||
"@nestjs/platform-express": "^11.1.3",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"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"
|
||||
|
|
@ -40,7 +36,6 @@
|
|||
"@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",
|
||||
|
|
@ -1384,6 +1379,7 @@
|
|||
"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"
|
||||
}
|
||||
|
|
@ -1392,6 +1388,7 @@
|
|||
"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"
|
||||
},
|
||||
|
|
@ -2455,29 +2452,6 @@
|
|||
"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",
|
||||
|
|
@ -2505,21 +2479,6 @@
|
|||
"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",
|
||||
|
|
@ -2711,13 +2670,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-express": {
|
||||
"version": "11.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.5.tgz",
|
||||
"integrity": "sha512-OsoiUBY9Shs5IG3uvDIt9/IDfY5OlvWBESuB/K4Eun8xILw1EK5d5qMfC3d2sIJ+kA3l+kBR1d/RuzH7VprLIg==",
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.3.tgz",
|
||||
"integrity": "sha512-hEDNMlaPiBO72fxxX/CuRQL3MEhKRc/sIYGVoXjrnw6hTxZdezvvM6A95UaLsYknfmcZZa/CdG1SMBZOu9agHQ==",
|
||||
"dependencies": {
|
||||
"cors": "2.8.5",
|
||||
"express": "5.1.0",
|
||||
"multer": "2.0.2",
|
||||
"multer": "2.0.1",
|
||||
"path-to-regexp": "8.2.0",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
|
|
@ -2882,18 +2841,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/typeorm": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz",
|
||||
"integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
||||
"reflect-metadata": "^0.1.13 || ^0.2.0",
|
||||
"rxjs": "^7.2.0",
|
||||
"typeorm": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||
|
|
@ -3556,16 +3503,6 @@
|
|||
"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",
|
||||
|
|
@ -3578,12 +3515,6 @@
|
|||
"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",
|
||||
|
|
@ -5490,11 +5421,6 @@
|
|||
"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",
|
||||
|
|
@ -6254,14 +6180,6 @@
|
|||
"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",
|
||||
|
|
@ -7240,9 +7158,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
|
||||
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz",
|
||||
"integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
|
|
@ -7440,13 +7358,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
|
||||
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
|
||||
"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.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.0.3",
|
||||
"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"
|
||||
|
|
@ -7483,6 +7402,7 @@
|
|||
"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"
|
||||
},
|
||||
|
|
@ -8008,6 +7928,7 @@
|
|||
"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"
|
||||
},
|
||||
|
|
@ -8872,46 +8793,6 @@
|
|||
"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",
|
||||
|
|
@ -9064,36 +8945,6 @@
|
|||
"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",
|
||||
|
|
@ -9106,11 +8957,6 @@
|
|||
"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",
|
||||
|
|
@ -9504,9 +9350,9 @@
|
|||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/multer": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
|
||||
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz",
|
||||
"integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==",
|
||||
"dependencies": {
|
||||
"append-field": "^1.0.0",
|
||||
"busboy": "^1.6.0",
|
||||
|
|
@ -9931,6 +9777,7 @@
|
|||
"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"
|
||||
|
|
@ -9946,6 +9793,7 @@
|
|||
"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"
|
||||
}
|
||||
|
|
@ -9986,87 +9834,6 @@
|
|||
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.16.3",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
|
||||
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.9.1",
|
||||
"pg-pool": "^3.10.1",
|
||||
"pg-protocol": "^1.10.3",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
|
||||
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
|
||||
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w=="
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
|
||||
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.10.3",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
|
||||
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ=="
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
|
@ -10222,41 +9989,6 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
|
@ -11088,6 +10820,7 @@
|
|||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,17 +26,13 @@
|
|||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/mongoose": "^11.0.3",
|
||||
"@nestjs/platform-express": "^11.1.3",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"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"
|
||||
|
|
@ -51,7 +47,6 @@
|
|||
"@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",
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { IsString, IsEnum, IsOptional, IsUUID, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { MetadataDto } from './metadataEntry.dto';
|
||||
import {MetadataDto} from './metadataEntry.dto';
|
||||
|
||||
export class BaseDto {
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
@IsOptional() // id is optional since BaseEntity generates it
|
||||
id?: string;
|
||||
|
||||
@IsEnum(['active', 'inactive'])
|
||||
@IsOptional()
|
||||
status?: 'active' | 'inactive';
|
||||
status?: 'active'| 'inactive';
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => MetadataDto)
|
||||
@IsOptional()
|
||||
metadata?: MetadataDto;
|
||||
metadata?: MetadataDto; // Optional, defaults to { entries: [] }
|
||||
}
|
||||
|
|
@ -14,3 +14,4 @@ export class MetadataDto {
|
|||
@Type(() => MetadataEntryDto)
|
||||
entries: MetadataEntryDto[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +1,47 @@
|
|||
import { Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn, BeforeInsert } from 'typeorm';
|
||||
import { Metadata } from './_metadata.entity';
|
||||
import { Prop, Schema } from '@nestjs/mongoose';
|
||||
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 {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@Prop({
|
||||
type: String,
|
||||
default: () => uuidv4(),
|
||||
required: true,
|
||||
unique: true,
|
||||
immutable: true,
|
||||
})
|
||||
id: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
@Prop({
|
||||
type: Date,
|
||||
default: () => new Date(),
|
||||
required: true,
|
||||
immutable: true,
|
||||
})
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
@Prop({
|
||||
type: Date,
|
||||
default: () => new Date(),
|
||||
required: true,
|
||||
})
|
||||
updatedAt: Date;
|
||||
|
||||
@Column(() => Metadata)
|
||||
metadata: Metadata = new Metadata();
|
||||
@Prop({
|
||||
type: MetadataSchema,
|
||||
default: () => ({ entries: [] }),
|
||||
required: true,
|
||||
})
|
||||
metadata: Metadata;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
@Prop({
|
||||
type: String,
|
||||
enum: ['active', 'inactive'],
|
||||
default: 'active',
|
||||
required: true,
|
||||
})
|
||||
status: 'active' | 'inactive';
|
||||
|
||||
@BeforeInsert()
|
||||
generateUuid() {
|
||||
if (!this.id) {
|
||||
this.id = uuidv4();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type BaseEntityDocument = BaseEntity & Document;
|
||||
|
|
@ -1,17 +1,28 @@
|
|||
import { Column, Entity } from 'typeorm';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Document } from 'mongoose';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Entity({ name: 'metadata' })
|
||||
@Schema({ _id: false }) // Embedded document
|
||||
export class Metadata {
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
primary: true,
|
||||
default: () => 'uuid_generate_v4()',
|
||||
@Prop({
|
||||
type: String,
|
||||
default: () => uuidv4(),
|
||||
required: true,
|
||||
immutable: true,
|
||||
})
|
||||
id?: string;
|
||||
id: string;
|
||||
|
||||
@Column({
|
||||
type: 'jsonb',
|
||||
default: () => "'[]'",
|
||||
@Prop({
|
||||
type: [
|
||||
{
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus,} from '@nestjs/common';
|
||||
import { AnswerService } from './answer.service';
|
||||
import { CreateAnswerDto } from './dto/create-answer.dto';
|
||||
import { UpdateAnswerDto } from './dto/update-answer.dto';
|
||||
|
|
@ -19,6 +19,7 @@ export class AnswerController {
|
|||
@Query('page') page: string = '1',
|
||||
@Query('limit') limit: string = '10',
|
||||
) {
|
||||
// The service returns the full pagination object
|
||||
return this.answerService.findAll(parseInt(page, 10), parseInt(limit, 10));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { AnswerService } from './answer.service';
|
||||
import { AnswerController } from './answer.controller';
|
||||
import { Answer } from './entity/answer.entity';
|
||||
import { Answer, AnswerSchema } from './entity/answer.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Answer]),
|
||||
MongooseModule.forFeature([
|
||||
{ name: Answer.name, schema: AnswerSchema },
|
||||
]),
|
||||
],
|
||||
controllers: [AnswerController],
|
||||
providers: [AnswerService],
|
||||
|
|
|
|||
|
|
@ -1,28 +1,25 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Answer } from './entity/answer.entity';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { Answer, AnswerDocument } from './entity/answer.entity';
|
||||
import { CreateAnswerDto } from './dto/create-answer.dto';
|
||||
import { UpdateAnswerDto } from './dto/update-answer.dto';
|
||||
import { PaginateModel } from '../participant/participant.service';
|
||||
|
||||
@Injectable()
|
||||
export class AnswerService {
|
||||
constructor(
|
||||
@InjectRepository(Answer)
|
||||
private readonly answerRepository: Repository<Answer>,
|
||||
@InjectModel(Answer.name)
|
||||
private readonly answerModel: PaginateModel<AnswerDocument>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateAnswerDto): Promise<Answer> {
|
||||
try {
|
||||
const answer = this.answerRepository.create({
|
||||
...data,
|
||||
metadata: data.metadata || { entries: [] },
|
||||
status: data.status || 'active',
|
||||
});
|
||||
return await this.answerRepository.save(answer);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create answer: ${error.message}`);
|
||||
}
|
||||
const answer = new this.answerModel({
|
||||
...data,
|
||||
id: data.id || undefined, // Let BaseEntity generate UUID if not provided
|
||||
metadata: data.metadata || { entries: [] },
|
||||
});
|
||||
return answer.save();
|
||||
}
|
||||
|
||||
async findAll(
|
||||
|
|
@ -39,62 +36,46 @@ export class AnswerService {
|
|||
nextPage: number | null;
|
||||
prevPage: number | null;
|
||||
}> {
|
||||
try {
|
||||
const skip = (page - 1) * limit;
|
||||
const [docs, totalDocs] = await this.answerRepository.findAndCount({
|
||||
skip,
|
||||
take: limit,
|
||||
});
|
||||
const totalPages = Math.ceil(totalDocs / limit);
|
||||
return {
|
||||
docs,
|
||||
totalDocs,
|
||||
limit,
|
||||
page,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPrevPage: page > 1,
|
||||
nextPage: page < totalPages ? page + 1 : null,
|
||||
prevPage: page > 1 ? page - 1 : null,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch answers: ${error.message}`);
|
||||
}
|
||||
return this.answerModel.paginate(
|
||||
{},
|
||||
{ page, limit, lean: true },
|
||||
);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Answer | null> {
|
||||
try {
|
||||
const answer = await this.answerRepository.findOne({ where: { id } });
|
||||
if (!answer) {
|
||||
throw new NotFoundException(`Answer with ID "${id}" not found`);
|
||||
}
|
||||
return answer;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to find answer: ${error.message}`);
|
||||
const answer = await this.answerModel
|
||||
.findOne({ id })
|
||||
.lean()
|
||||
.exec();
|
||||
if (!answer) {
|
||||
throw new NotFoundException(`Answer with ID "${id}" not found`);
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateAnswerDto): Promise<Answer | null> {
|
||||
try {
|
||||
await this.answerRepository.update({ id }, { ...data, updatedAt: new Date() });
|
||||
const updatedAnswer = await this.answerRepository.findOne({ where: { id } });
|
||||
if (!updatedAnswer) {
|
||||
throw new NotFoundException(`Answer with ID "${id}" not found`);
|
||||
}
|
||||
return updatedAnswer;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update answer: ${error.message}`);
|
||||
async update(
|
||||
id: string,
|
||||
data: UpdateAnswerDto,
|
||||
): Promise<Answer | null> {
|
||||
const updatedAnswer = await this.answerModel
|
||||
.findOneAndUpdate(
|
||||
{ id },
|
||||
{ $set: { ...data, updatedAt: new Date() } },
|
||||
{ new: true },
|
||||
)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (!updatedAnswer) {
|
||||
throw new NotFoundException(`Answer with ID "${id}" not found`);
|
||||
}
|
||||
return updatedAnswer;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
try {
|
||||
const result = await this.answerRepository.delete({ id });
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Answer with ID "${id}" not found`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete answer: ${error.message}`);
|
||||
const result = await this.answerModel.deleteOne({ id }).exec();
|
||||
if (result.deletedCount === 0) {
|
||||
throw new NotFoundException(`Answer with ID "${id}" not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { IsString, IsUUID } from 'class-validator';
|
||||
import { IsString, IsUUID} from 'class-validator';
|
||||
import { BaseDto } from '../../_core/dto/base.dto';
|
||||
|
||||
export class CreateAnswerDto extends BaseDto {
|
||||
export class CreateAnswerDto extends BaseDto{
|
||||
@IsUUID()
|
||||
participantId: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,44 @@
|
|||
import { Column, Entity, ManyToOne } from 'typeorm';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Document } from 'mongoose';
|
||||
import { BaseEntity } from 'src/_core/entity/_base.entity';
|
||||
import { Question } from '../../question/entity/question.entity';
|
||||
import * as mongoosePaginate from 'mongoose-paginate-v2';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Entity({ name: 'answers' })
|
||||
@Schema({ _id: false, id: false }) // Disable _id and virtual id
|
||||
export class Answer extends BaseEntity {
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
@Prop({
|
||||
type: String,
|
||||
default: () => uuidv4(),
|
||||
required: true,
|
||||
immutable: true,
|
||||
})
|
||||
participantId: string;
|
||||
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
@Prop({
|
||||
type: String,
|
||||
default: () => uuidv4(),
|
||||
required: true,
|
||||
immutable: true,
|
||||
})
|
||||
questionId: string;
|
||||
questionId: string; // form can be identified based on question
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
value: string; //value can be mapped to option based on question type
|
||||
|
||||
@Column()
|
||||
value: string;
|
||||
|
||||
@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;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,24 +1,12 @@
|
|||
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,
|
||||
@InjectDataSource() private readonly dataSource: DataSource) {}
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@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 };
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { ParticipantModule } from './participant/participant.module';
|
||||
|
|
@ -13,15 +14,13 @@ import { FormSectionModule } from './formSection/formSection.module';
|
|||
import { OptionModule } from './option/option.module';
|
||||
import { ParticipanGrouptModule } from './participantGroup/participantGroup.module';
|
||||
import { RealmModule } from './realm/realm.module';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { typeOrmConfig } from './config/database.config';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forRoot(typeOrmConfig),
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true, // Makes ConfigModule available globally
|
||||
}),
|
||||
MongooseModule.forRoot(process.env.MONGODB_URI || 'mongodb://localhost:27017/db'),
|
||||
ParticipantModule,
|
||||
QuestionModule,
|
||||
AnswerModule,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { Controller, Get, Post, Patch, Delete, Param, Body, UsePipes, ValidationPipe, Query, ParseUUIDPipe, HttpCode, HttpStatus,} from '@nestjs/common';
|
||||
import { AttachmentService } from './attachment.service';
|
||||
import { CreateAttachmentDto } from './dto/create_attachment.dto';
|
||||
import { UpdateAttachmentDto } from './dto/update_attachment.dto';
|
||||
|
|
@ -19,6 +19,7 @@ export class AttachmentController {
|
|||
@Query('page') page: string = '1',
|
||||
@Query('limit') limit: string = '10',
|
||||
) {
|
||||
// The service returns the full pagination object
|
||||
return this.attachmentService.findAll(parseInt(page, 10), parseInt(limit, 10));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Attachment } from './entity/attachment.entity';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { Attachment, AttachmentSchema } from './entity/attachment.entity';
|
||||
import { AttachmentController } from './attachment.controller';
|
||||
import { AttachmentService } from './attachment.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Attachment]),
|
||||
MongooseModule.forFeature([
|
||||
{ name: Attachment.name, schema: AttachmentSchema },
|
||||
]),
|
||||
],
|
||||
controllers: [AttachmentController],
|
||||
providers: [AttachmentService],
|
||||
|
|
|
|||
|
|
@ -1,60 +1,26 @@
|
|||
import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Attachment } from './entity/attachment.entity';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { Attachment, AttachmentDocument } from './entity/attachment.entity';
|
||||
import { CreateAttachmentDto } from './dto/create_attachment.dto';
|
||||
import { UpdateAttachmentDto } from './dto/update_attachment.dto';
|
||||
import axios from 'axios';
|
||||
import { decode } from 'jsonwebtoken';
|
||||
import { PaginateModel } from '../participant/participant.service';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class AttachmentService {
|
||||
constructor(
|
||||
@InjectRepository(Attachment)
|
||||
private readonly attachmentRepository: Repository<Attachment>,
|
||||
@InjectModel(Attachment.name)
|
||||
private readonly attachmentModel: PaginateModel<AttachmentDocument>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateAttachmentDto): Promise<Attachment> {
|
||||
try {
|
||||
// Here we send a sample request, in final implementation, there'll be no need for that, we'll get the decoded token
|
||||
const authResponse = await axios.post(
|
||||
'https://auth.didvan.com/realms/didvan/protocol/openid-connect/token',
|
||||
new URLSearchParams({
|
||||
client_id: 'didvan-app',
|
||||
username: 'bob',
|
||||
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 attachment with ownerId set to sub
|
||||
const attachment = this.attachmentRepository.create({
|
||||
...data,
|
||||
ownerId: decodedToken.sub, // Set ownerId to the token's sub
|
||||
metadata: data.metadata
|
||||
? { entries: data.metadata.entries }
|
||||
: { entries: [] },
|
||||
status: data.status || 'active',
|
||||
});
|
||||
|
||||
// Save the attachment to the database
|
||||
return await this.attachmentRepository.save(attachment);
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(`Failed to create attachment: ${error.message}`);
|
||||
}
|
||||
const attachment = new this.attachmentModel({
|
||||
...data,
|
||||
id: data.id || undefined, // Let BaseEntity generate UUID if not provided
|
||||
metadata: data.metadata || { entries: [] },
|
||||
});
|
||||
return attachment.save();
|
||||
}
|
||||
|
||||
async findAll(
|
||||
|
|
@ -71,62 +37,47 @@ export class AttachmentService {
|
|||
nextPage: number | null;
|
||||
prevPage: number | null;
|
||||
}> {
|
||||
try {
|
||||
const skip = (page - 1) * limit;
|
||||
const [docs, totalDocs] = await this.attachmentRepository.findAndCount({
|
||||
skip,
|
||||
take: limit,
|
||||
});
|
||||
const totalPages = Math.ceil(totalDocs / limit);
|
||||
return {
|
||||
docs,
|
||||
totalDocs,
|
||||
limit,
|
||||
page,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPrevPage: page > 1,
|
||||
nextPage: page < totalPages ? page + 1 : null,
|
||||
prevPage: page > 1 ? page - 1 : null,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to fetch attachments: ${error.message}`);
|
||||
}
|
||||
// Selects all fields from the Attachment entity for the response
|
||||
return this.attachmentModel.paginate(
|
||||
{},
|
||||
{ page, limit, lean: true },
|
||||
);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Attachment | null> {
|
||||
try {
|
||||
const attachment = await this.attachmentRepository.findOne({ where: { id } });
|
||||
if (!attachment) {
|
||||
throw new NotFoundException(`Attachment with ID "${id}" not found`);
|
||||
}
|
||||
return attachment;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to find attachment: ${error.message}`);
|
||||
const attachment = await this.attachmentModel
|
||||
.findOne({ id })
|
||||
.lean()
|
||||
.exec();
|
||||
if (!attachment) {
|
||||
throw new NotFoundException(`Attachment with ID "${id}" not found`);
|
||||
}
|
||||
return attachment;
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateAttachmentDto): Promise<Attachment | null> {
|
||||
try {
|
||||
await this.attachmentRepository.update({ id }, { ...data, updatedAt: new Date() });
|
||||
const updatedAttachment = await this.attachmentRepository.findOne({ where: { id } });
|
||||
if (!updatedAttachment) {
|
||||
throw new NotFoundException(`Attachment with ID "${id}" not found`);
|
||||
}
|
||||
return updatedAttachment;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update attachment: ${error.message}`);
|
||||
async update(
|
||||
id: string,
|
||||
data: UpdateAttachmentDto,
|
||||
): Promise<Attachment | null> {
|
||||
const updatedAttachment = await this.attachmentModel
|
||||
.findOneAndUpdate(
|
||||
{ id },
|
||||
{ $set: { ...data, updatedAt: new Date() } },
|
||||
{ new: true },
|
||||
)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (!updatedAttachment) {
|
||||
throw new NotFoundException(`Attachment with ID "${id}" not found`);
|
||||
}
|
||||
return updatedAttachment;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
try {
|
||||
const result = await this.attachmentRepository.delete({ id });
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Attachment with ID "${id}" not found`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete attachment: ${error.message}`);
|
||||
const result = await this.attachmentModel.deleteOne({ id }).exec();
|
||||
if (result.deletedCount === 0) {
|
||||
throw new NotFoundException(`Attachment with ID "${id}" not found`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
import { IsEnum, IsOptional, IsString, IsUrl, IsUUID, ValidateNested } from 'class-validator';
|
||||
// DTO for embedded Attachment
|
||||
import { isEnum, IsEnum, IsOptional, IsString, IsUrl, IsUUID, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { BaseDto } from '../../_core/dto/base.dto';
|
||||
|
||||
export class CreateAttachmentDto extends BaseDto {
|
||||
export class CreateAttachmentDto extends BaseDto{
|
||||
@IsString()
|
||||
displayName: string;
|
||||
|
||||
@IsString()
|
||||
filePath: string;
|
||||
|
||||
@IsEnum(['png', 'pdf', 'csv', 'jpg'])
|
||||
fileType: 'png' | 'pdf' | 'csv' | 'jpg';
|
||||
@IsEnum(['png','pdf','csv','jpg'])
|
||||
fileType: 'png'|'pdf'|'csv'|'jpg';
|
||||
|
||||
@IsUrl()
|
||||
storageUrl: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
ownerId?: string;
|
||||
ownerId: string;
|
||||
}
|
||||
|
|
@ -1,37 +1,40 @@
|
|||
import { Column, Entity, ManyToOne } from 'typeorm';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Document } from 'mongoose';
|
||||
import { BaseEntity } from 'src/_core/entity/_base.entity';
|
||||
import { Question } from 'src/question/entity/question.entity';
|
||||
import { FormSection } from '../../formSection/entity/formSection.entity';
|
||||
import { Form } from '../../form/entity/form.entity';
|
||||
|
||||
@Entity({ name: 'attachments' })
|
||||
@Schema({ _id: false, id: false }) // Embedded document, inheriting from BaseEntity
|
||||
export class Attachment extends BaseEntity {
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
displayName: string;
|
||||
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
filePath: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
@Prop({
|
||||
type: String,
|
||||
enum: ['png', 'pdf', 'csv', 'jpg'],
|
||||
required: true,
|
||||
})
|
||||
fileType: 'png' | 'pdf' | 'csv' | 'jpg';
|
||||
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
storageUrl: string;
|
||||
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
ownerId: string;
|
||||
|
||||
@ManyToOne(() => Question, (question) => question.attachments)
|
||||
question: Question;
|
||||
|
||||
@ManyToOne(() => FormSection, formSection => formSection.attachments)
|
||||
formSection: FormSection;
|
||||
|
||||
@ManyToOne(() => Form, form => form.attachments)
|
||||
form: Form;
|
||||
ownerId: string; // UUID of the user who uploaded it
|
||||
}
|
||||
|
||||
export type AttachmentDocument = Attachment & Document;
|
||||
export const AttachmentSchema = SchemaFactory.createForClass(Attachment);
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
// 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';
|
||||
import { BaseEntity } from '../_core/entity/_base.entity';
|
||||
|
||||
export const typeOrmConfig: TypeOrmModuleOptions = {
|
||||
type: 'postgres',
|
||||
host: 'localhost',
|
||||
port: 5433,
|
||||
username: 'postgres',
|
||||
password: '1111',
|
||||
database: 'db',
|
||||
entities: [
|
||||
BaseEntity,
|
||||
Participant,
|
||||
Metadata,
|
||||
Form,
|
||||
Realm,
|
||||
Question,
|
||||
Answer,
|
||||
Option,
|
||||
Attachment,
|
||||
FormSection,
|
||||
FormPage,
|
||||
FormResult,
|
||||
ParticipantGroup,
|
||||
],
|
||||
synchronize: true,
|
||||
logging: true,
|
||||
};
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { IsString, IsOptional, ValidateNested, IsArray } from 'class-validator';
|
||||
import { IsString, IsOptional, IsUUID, ValidateNested, IsArray, IsEnum, IsDateString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { BaseDto } from '../../_core/dto/base.dto';
|
||||
import { CreateAttachmentDto } from '../../attachment/dto/create_attachment.dto';
|
||||
import { CreateQuestionDto } from '../../question/dto/create-question.dto';
|
||||
import { CreateFormPageDto } from '../../formPage/dto/create-formPage.dto';
|
||||
import { CreateParticipantDto } from '../../participant/dto/create-participant.dto';
|
||||
import { CreateAttachmentDto } from '../../attachment/dto/create_attachment.dto';
|
||||
|
||||
export class CreateFormDto extends BaseDto {
|
||||
@IsString()
|
||||
|
|
@ -16,17 +17,15 @@ export class CreateFormDto extends BaseDto {
|
|||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateAttachmentDto)
|
||||
@IsOptional()
|
||||
attachments?: CreateAttachmentDto[]; // Keep if attachments are still part of the DTO
|
||||
attachments?: CreateAttachmentDto[];
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateFormPageDto)
|
||||
@IsOptional()
|
||||
pages?: CreateFormPageDto[];
|
||||
pages: CreateFormPageDto[];
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateParticipantDto)
|
||||
@IsOptional()
|
||||
participants?: CreateParticipantDto[];
|
||||
participants: CreateParticipantDto[];
|
||||
}
|
||||
|
|
@ -1,32 +1,58 @@
|
|||
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';
|
||||
import { Realm } from '../../realm/entity/realm.entity';
|
||||
import { Attachment } from '../../attachment/entity/attachment.entity';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Document } from 'mongoose';
|
||||
import { BaseEntity } from 'src/_core/entity/_base.entity';
|
||||
import * as mongoosePaginate from 'mongoose-paginate-v2';
|
||||
import { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity';
|
||||
import { Option, OptionSchema } from '../../option/entity/option.entity';
|
||||
import { Answer, AnswerSchema } from '../../answer/entity/answer.entity';
|
||||
import { Question, QuestionSchema } from '../../question/entity/question.entity';
|
||||
import { Participant, ParticipantSchema } from '../../participant/entity/participant.entity';
|
||||
import { FormPage, FormPageSchema } from '../../formPage/entity/formPage.entity';
|
||||
|
||||
@Entity()
|
||||
@Schema({ _id: false, id: false }) // Disable _id and virtual id
|
||||
export class Form extends BaseEntity {
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
title: string;
|
||||
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
description: string;
|
||||
|
||||
// One-to-Many relationship with FormPage
|
||||
@OneToMany(() => FormPage, formPage => formPage.form, { cascade: true, eager: true })
|
||||
pages: FormPage[];
|
||||
|
||||
// One-to-Many relationship with Participant
|
||||
@OneToMany(() => Participant, participant => participant.form, { cascade: true, eager: true })
|
||||
participants: Participant[];
|
||||
|
||||
|
||||
// One-to-Many relationship with Attachment
|
||||
@OneToMany(() => Attachment, attachment => attachment.form, { cascade: true, eager: true })
|
||||
@Prop({
|
||||
type: [AttachmentSchema],
|
||||
default: [],
|
||||
})
|
||||
attachments: Attachment[];
|
||||
|
||||
@ManyToOne(() => Realm, realm => realm.forms, { /*cascade: true, eager: true */})
|
||||
realm: Realm;
|
||||
@Prop({
|
||||
type: [FormPageSchema],
|
||||
default: [],
|
||||
})
|
||||
pages: FormPage[];
|
||||
|
||||
@Prop({
|
||||
type: [ParticipantSchema],
|
||||
default: [],
|
||||
})
|
||||
participants: Participant[];
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export class FormController {
|
|||
@Query('page') page: string = '1',
|
||||
@Query('limit') limit: string = '10',
|
||||
) {
|
||||
// The service returns the full pagination object
|
||||
return this.formService.findAll(parseInt(page, 10), parseInt(limit, 10));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { FormService } from './form.service';
|
||||
import { FormController } from './form.controller';
|
||||
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
|
||||
import { Form, FormSchema } from './entity/form.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Form,
|
||||
FormPage,
|
||||
FormSection,
|
||||
Attachment,
|
||||
Question,
|
||||
Answer,
|
||||
Option,
|
||||
Participant, // Include Participant entity
|
||||
MongooseModule.forFeature([
|
||||
{ name: Form.name, schema: FormSchema },
|
||||
]),
|
||||
],
|
||||
controllers: [FormController],
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Form } from './entity/form.entity';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { Form, FormDocument } from './entity/form.entity';
|
||||
import { CreateFormDto } from './dto/create-form.dto';
|
||||
import { UpdateFormDto } from './dto/update-form.dto';
|
||||
import { PaginateModel } from '../participant/participant.service';
|
||||
|
||||
@Injectable()
|
||||
export class FormService {
|
||||
constructor(
|
||||
@InjectRepository(Form)
|
||||
private readonly formRepo: Repository<Form>,
|
||||
@InjectModel(Form.name)
|
||||
private readonly formModel: PaginateModel<FormDocument>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateFormDto): Promise<Form> {
|
||||
const form = this.formRepo.create({
|
||||
const form = new this.formModel({
|
||||
...data,
|
||||
id: data.id || undefined, // Let BaseEntity generate UUID if not provided
|
||||
metadata: data.metadata || { entries: [] },
|
||||
});
|
||||
return await this.formRepo.save(form);
|
||||
return form.save();
|
||||
}
|
||||
|
||||
async findAll(
|
||||
|
|
@ -33,47 +36,18 @@ export class FormService {
|
|||
nextPage: number | null;
|
||||
prevPage: number | null;
|
||||
}> {
|
||||
const [docs, totalDocs] = await this.formRepo.findAndCount({
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
relations: [
|
||||
'pages',
|
||||
'pages.formSections',
|
||||
'pages.formSections.attachments',
|
||||
'pages.formSections.questions',
|
||||
'pages.formSections.questions.options',
|
||||
'pages.formSections.questions.answers',
|
||||
'participants', // Load participants
|
||||
], // Load nested relations
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(totalDocs / limit);
|
||||
return {
|
||||
docs,
|
||||
totalDocs,
|
||||
limit,
|
||||
page,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPrevPage: page > 1,
|
||||
nextPage: page < totalPages ? page + 1 : null,
|
||||
prevPage: page > 1 ? page - 1 : null,
|
||||
};
|
||||
// Selects all fields from the Form entity for the response
|
||||
return this.formModel.paginate(
|
||||
{},
|
||||
{ page, limit, lean: true },
|
||||
);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Form | null> {
|
||||
const form = await this.formRepo.findOne({
|
||||
where: { id },
|
||||
relations: [
|
||||
'pages',
|
||||
'pages.formSections',
|
||||
'pages.formSections.attachments',
|
||||
'pages.formSections.questions',
|
||||
'pages.formSections.questions.options',
|
||||
'pages.formSections.questions.answers',
|
||||
'participants',
|
||||
], // Load nested relations
|
||||
});
|
||||
const form = await this.formModel
|
||||
.findOne({ id })
|
||||
.lean()
|
||||
.exec();
|
||||
if (!form) {
|
||||
throw new NotFoundException(`Form with ID "${id}" not found`);
|
||||
}
|
||||
|
|
@ -84,20 +58,24 @@ export class FormService {
|
|||
id: string,
|
||||
data: UpdateFormDto,
|
||||
): Promise<Form | null> {
|
||||
const result = await this.formRepo.update(
|
||||
{ id },
|
||||
{ ...data, updatedAt: new Date() },
|
||||
);
|
||||
const updatedForm = await this.formModel
|
||||
.findOneAndUpdate(
|
||||
{ id },
|
||||
{ $set: { ...data, updatedAt: new Date() } },
|
||||
{ new: true },
|
||||
)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (result.affected === 0) {
|
||||
if (!updatedForm) {
|
||||
throw new NotFoundException(`Form with ID "${id}" not found`);
|
||||
}
|
||||
return await this.findById(id);
|
||||
return updatedForm;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.formRepo.delete({ id });
|
||||
if (result.affected === 0) {
|
||||
const result = await this.formModel.deleteOne({ id }).exec();
|
||||
if (result.deletedCount === 0) {
|
||||
throw new NotFoundException(`Form with ID "${id}" not found`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,45 @@
|
|||
import { Entity, Column, OneToMany, ManyToOne } from 'typeorm';
|
||||
import { BaseEntity } from '../../_core/entity/_base.entity';
|
||||
import { FormSection } from '../../formSection/entity/formSection.entity';
|
||||
import { Form } from '../../form/entity/form.entity';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Document } from 'mongoose';
|
||||
import { BaseEntity } from 'src/_core/entity/_base.entity';
|
||||
import * as mongoosePaginate from 'mongoose-paginate-v2';
|
||||
import { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity';
|
||||
import { Option, OptionSchema } from '../../option/entity/option.entity';
|
||||
import { Answer, AnswerSchema } from '../../answer/entity/answer.entity';
|
||||
import { Question, QuestionSchema } from '../../question/entity/question.entity';
|
||||
import { FormSection, FormSectionSchema } from '../../formSection/entity/formSection.entity';
|
||||
|
||||
@Entity()
|
||||
@Schema({ _id: false, id: false }) // Disable _id and virtual id
|
||||
export class FormPage extends BaseEntity {
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
title: string;
|
||||
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
description: string;
|
||||
|
||||
// One-to-Many relationship with FormSection
|
||||
@OneToMany(() => FormSection, formSection => formSection.formPage, { cascade: true, eager: true })
|
||||
formSections: FormSection[];
|
||||
|
||||
// Many-to-One relationship with Form
|
||||
@ManyToOne(() => Form, form => form.pages) // Ensure 'pages' matches the property name in Form entity
|
||||
form: Form;
|
||||
|
||||
@Prop({
|
||||
type: [FormSectionSchema],
|
||||
default: [],
|
||||
})
|
||||
attachments: FormSection[];
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export class FormPageController {
|
|||
@Query('page') page: string = '1',
|
||||
@Query('limit') limit: string = '10',
|
||||
) {
|
||||
// The service returns the full pagination object
|
||||
return this.formPageService.findAll(parseInt(page, 10), parseInt(limit, 10));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { FormPageService } from './formPage.service';
|
||||
import { FormPageController } from './formPage.controller';
|
||||
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';
|
||||
import { FormPage, FormPageSchema } from './entity/formPage.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
FormPage,
|
||||
FormSection,
|
||||
Attachment,
|
||||
Question,
|
||||
Answer,
|
||||
Option,
|
||||
MongooseModule.forFeature([
|
||||
{ name: FormPage.name, schema: FormPageSchema },
|
||||
]),
|
||||
],
|
||||
controllers: [FormPageController],
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { FormPage } from './entity/formPage.entity';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { FormPage, FormPageDocument } from './entity/formPage.entity';
|
||||
import { CreateFormPageDto } from './dto/create-formPage.dto';
|
||||
import { UpdateFormPageDto } from './dto/update-formPage.dto';
|
||||
import { PaginateModel } from '../participant/participant.service';
|
||||
|
||||
@Injectable()
|
||||
export class FormPageService {
|
||||
constructor(
|
||||
@InjectRepository(FormPage)
|
||||
private readonly formPageRepo: Repository<FormPage>,
|
||||
@InjectModel(FormPage.name)
|
||||
private readonly formPageModel: PaginateModel<FormPageDocument>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateFormPageDto): Promise<FormPage> {
|
||||
const formPage = this.formPageRepo.create({
|
||||
const formPage = new this.formPageModel({
|
||||
...data,
|
||||
id: data.id || undefined, // Let BaseEntity generate UUID if not provided
|
||||
metadata: data.metadata || { entries: [] },
|
||||
});
|
||||
return await this.formPageRepo.save(formPage);
|
||||
return formPage.save();
|
||||
}
|
||||
|
||||
async findAll(
|
||||
|
|
@ -33,31 +36,18 @@ export class FormPageService {
|
|||
nextPage: number | null;
|
||||
prevPage: number | null;
|
||||
}> {
|
||||
const [docs, totalDocs] = await this.formPageRepo.findAndCount({
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
relations: ['formSections', 'formSections.attachments', 'formSections.questions', 'formSections.questions.options', 'formSections.questions.answers'], // Load related data
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(totalDocs / limit);
|
||||
return {
|
||||
docs,
|
||||
totalDocs,
|
||||
limit,
|
||||
page,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPrevPage: page > 1,
|
||||
nextPage: page < totalPages ? page + 1 : null,
|
||||
prevPage: page > 1 ? page - 1 : null,
|
||||
};
|
||||
// Selects all fields from the FormPage entity for the response
|
||||
return this.formPageModel.paginate(
|
||||
{},
|
||||
{ page, limit, lean: true },
|
||||
);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<FormPage | null> {
|
||||
const formPage = await this.formPageRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['formSections', 'formSections.attachments', 'formSections.questions', 'formSections.questions.options', 'formSections.questions.answers'], // Load related data
|
||||
});
|
||||
const formPage = await this.formPageModel
|
||||
.findOne({ id })
|
||||
.lean()
|
||||
.exec();
|
||||
if (!formPage) {
|
||||
throw new NotFoundException(`FormPage with ID "${id}" not found`);
|
||||
}
|
||||
|
|
@ -68,20 +58,24 @@ export class FormPageService {
|
|||
id: string,
|
||||
data: UpdateFormPageDto,
|
||||
): Promise<FormPage | null> {
|
||||
const result = await this.formPageRepo.update(
|
||||
{ id },
|
||||
{ ...data, updatedAt: new Date() },
|
||||
);
|
||||
const updatedFormPage = await this.formPageModel
|
||||
.findOneAndUpdate(
|
||||
{ id },
|
||||
{ $set: { ...data, updatedAt: new Date() } },
|
||||
{ new: true },
|
||||
)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (result.affected === 0) {
|
||||
if (!updatedFormPage) {
|
||||
throw new NotFoundException(`FormPage with ID "${id}" not found`);
|
||||
}
|
||||
return await this.findById(id);
|
||||
return updatedFormPage;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.formPageRepo.delete({ id });
|
||||
if (result.affected === 0) {
|
||||
const result = await this.formPageModel.deleteOne({ id }).exec();
|
||||
if (result.deletedCount === 0) {
|
||||
throw new NotFoundException(`FormPage with ID "${id}" not found`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (remains the same as it's validation-focused)
|
||||
// DTO for Options
|
||||
class CreateOptionsDto {
|
||||
@IsString()
|
||||
value: string;
|
||||
|
|
@ -11,7 +11,7 @@ class CreateOptionsDto {
|
|||
count: number;
|
||||
}
|
||||
|
||||
// DTO for Opinion (remains the same)
|
||||
// DTO for Opinion
|
||||
class CreateOpinionDto {
|
||||
@ValidateNested()
|
||||
@Type(() => CreateOptionsDto)
|
||||
|
|
@ -21,7 +21,7 @@ class CreateOpinionDto {
|
|||
questionId: string;
|
||||
}
|
||||
|
||||
// Main DTO for creating a FormResult (remains largely the same)
|
||||
// Main DTO for creating a FormResult
|
||||
export class CreateFormResultDto extends BaseDto {
|
||||
@IsNumber()
|
||||
numParticipants: number;
|
||||
|
|
|
|||
|
|
@ -1,41 +1,98 @@
|
|||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from '../../_core/entity/_base.entity';
|
||||
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';
|
||||
|
||||
// Options class (embedded)
|
||||
export class Options {
|
||||
@Column()
|
||||
// Sub-schema for Options
|
||||
@Schema({ _id: false, id: false })
|
||||
class Options {
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
value: string;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
@Prop({
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
default: 0
|
||||
})
|
||||
count: number;
|
||||
}
|
||||
|
||||
// Opinion class (embedded)
|
||||
export class Opinion {
|
||||
@Column(() => Options)
|
||||
export const OptionsSchema = SchemaFactory.createForClass(Options);
|
||||
|
||||
// Sub-schema for Opinion
|
||||
@Schema({ _id: false, id: false })
|
||||
class Opinion {
|
||||
@Prop({
|
||||
type: OptionsSchema,
|
||||
required: true,
|
||||
})
|
||||
options: Options;
|
||||
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
unique: true,
|
||||
@Prop({
|
||||
type: UUID,
|
||||
required: true,
|
||||
})
|
||||
questionId: string;
|
||||
}
|
||||
export const OpinionSchema = SchemaFactory.createForClass(Opinion);
|
||||
|
||||
@Entity()
|
||||
|
||||
@Schema({ _id: false, id: false }) // Disable _id and virtual id
|
||||
export class FormResult extends BaseEntity {
|
||||
@Column({ type: 'int', default: 0 })
|
||||
numParticipants: number;
|
||||
@Prop({
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
default: 0,
|
||||
})
|
||||
numParticipants: string;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
numQuestions: number;
|
||||
@Prop({
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
default: 0,
|
||||
})
|
||||
numQuestions: string;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
numAnswers: number;
|
||||
@Prop({
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
default: 0,
|
||||
})
|
||||
numAnswers: string;
|
||||
|
||||
@Column({ type: 'int', default: 0 })
|
||||
numComplete: number;
|
||||
@Prop({
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 0,
|
||||
default: 0,
|
||||
})
|
||||
numComplete: string; // number of people who completed this form
|
||||
|
||||
@Column(() => Opinion)
|
||||
@Prop({
|
||||
type: [OpinionSchema],
|
||||
default: [],
|
||||
})
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ 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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { FormResultService } from './formResult.service';
|
||||
import { FormResultController } from './formResult.controller';
|
||||
import { FormResult } from './entity/formResult.entity';
|
||||
import { FormResult, FormResultSchema } from './entity/formResult.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
FormResult,
|
||||
MongooseModule.forFeature([
|
||||
{ name: FormResult.name, schema: FormResultSchema },
|
||||
]),
|
||||
],
|
||||
controllers: [FormResultController],
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { FormResult } from './entity/formResult.entity';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { FormResult, FormResultDocument } 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(
|
||||
@InjectRepository(FormResult)
|
||||
private readonly formResultRepo: Repository<FormResult>,
|
||||
@InjectModel(FormResult.name)
|
||||
private readonly formResultModel: PaginateModel<FormResultDocument>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateFormResultDto): Promise<FormResult> {
|
||||
const formResult = this.formResultRepo.create({
|
||||
const formResult = new this.formResultModel({
|
||||
...data,
|
||||
id: data.id || undefined, // Let BaseEntity generate UUID if not provided
|
||||
metadata: data.metadata || { entries: [] },
|
||||
});
|
||||
return await this.formResultRepo.save(formResult);
|
||||
return formResult.save();
|
||||
}
|
||||
|
||||
async findAll(
|
||||
|
|
@ -33,31 +36,18 @@ export class FormResultService {
|
|||
nextPage: number | null;
|
||||
prevPage: number | null;
|
||||
}> {
|
||||
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,
|
||||
};
|
||||
// Selects all fields from the FormResult entity for the response
|
||||
return this.formResultModel.paginate(
|
||||
{},
|
||||
{ page, limit, lean: true },
|
||||
);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<FormResult | null> {
|
||||
const formResult = await this.formResultRepo.findOne({
|
||||
where: { id },
|
||||
// No specific relations needed here unless FormResult links to other entities
|
||||
});
|
||||
const formResult = await this.formResultModel
|
||||
.findOne({ id })
|
||||
.lean()
|
||||
.exec();
|
||||
if (!formResult) {
|
||||
throw new NotFoundException(`FormResult with ID "${id}" not found`);
|
||||
}
|
||||
|
|
@ -68,20 +58,24 @@ export class FormResultService {
|
|||
id: string,
|
||||
data: UpdateFormResultDto,
|
||||
): Promise<FormResult | null> {
|
||||
const result = await this.formResultRepo.update(
|
||||
{ id },
|
||||
{ ...data, updatedAt: new Date() },
|
||||
);
|
||||
const updatedFormResult = await this.formResultModel
|
||||
.findOneAndUpdate(
|
||||
{ id },
|
||||
{ $set: { ...data, updatedAt: new Date() } },
|
||||
{ new: true },
|
||||
)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (result.affected === 0) {
|
||||
if (!updatedFormResult) {
|
||||
throw new NotFoundException(`FormResult with ID "${id}" not found`);
|
||||
}
|
||||
return await this.findById(id);
|
||||
return updatedFormResult;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.formResultRepo.delete({ id });
|
||||
if (result.affected === 0) {
|
||||
const result = await this.formResultModel.deleteOne({ id }).exec();
|
||||
if (result.deletedCount === 0) {
|
||||
throw new NotFoundException(`FormResult with ID "${id}" not found`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,85 @@
|
|||
// src/formSection/entity/formSection.entity.ts
|
||||
import { Entity, Column, OneToMany, ManyToOne } from 'typeorm'; // Removed Embeddable import
|
||||
import { BaseEntity } from '../../_core/entity/_base.entity';
|
||||
import { Attachment } from '../../attachment/entity/attachment.entity';
|
||||
import { Question } from '../../question/entity/question.entity';
|
||||
import { FormPage } from 'src/formPage/entity/formPage.entity';
|
||||
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';
|
||||
|
||||
// DisplayCondition is now a regular class, embedded using @Column(() => DisplayCondition)
|
||||
// Sub-schema for DisplayContition
|
||||
@Schema({ _id: false, id: false })
|
||||
export class DisplayCondition {
|
||||
@Column()
|
||||
answer: string; // Assuming this is an ID reference to an Answer entity
|
||||
|
||||
@Column()
|
||||
question: string; // Assuming this is an ID reference to a Question entity
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['equal', 'contains', 'not_equal', 'not_contains'],
|
||||
@Prop({
|
||||
type: [AnswerSchema],
|
||||
required: true,
|
||||
})
|
||||
relation: 'equal' | 'contains' | 'not_equal' | 'not_contains';
|
||||
answer: Answer;
|
||||
|
||||
@Prop({
|
||||
type: [QuestionSchema],
|
||||
required: true,
|
||||
})
|
||||
question: Question;
|
||||
|
||||
@Prop({
|
||||
type: String,
|
||||
enum: ['equal','contains','not_equal','not_contains'],
|
||||
required: true,
|
||||
})
|
||||
relation: 'equal'|'contains'|'not_equal'|'not_contains';
|
||||
}
|
||||
|
||||
@Entity()
|
||||
export const DisplayConditionSchema = SchemaFactory.createForClass(DisplayCondition);
|
||||
|
||||
|
||||
//\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\
|
||||
|
||||
@Schema({ _id: false, id: false }) // Disable _id and virtual id
|
||||
export class FormSection extends BaseEntity {
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
title: string;
|
||||
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
description: string;
|
||||
|
||||
// One-to-Many relationship with Attachment
|
||||
@OneToMany(() => Attachment, attachment => attachment.formSection, { cascade: true, eager: true })
|
||||
@Prop({
|
||||
type: [AttachmentSchema],
|
||||
default: [],
|
||||
})
|
||||
attachments: Attachment[];
|
||||
|
||||
// Using @Column(() => DisplayCondition) for embedded array of objects
|
||||
@Column(() => DisplayCondition)
|
||||
displayCondition: DisplayCondition[];
|
||||
@Prop({
|
||||
type: [DisplayContidionSchema],
|
||||
default: [],
|
||||
})
|
||||
displayCondition: DisplayContidion[];
|
||||
|
||||
// One-to-Many relationship with Question
|
||||
@OneToMany(() => Question, question => question.formSection, { cascade: true, eager: true })
|
||||
@Prop({
|
||||
type: [QuestionSchema],
|
||||
default: [],
|
||||
})
|
||||
questions: Question[];
|
||||
|
||||
// Many-to-One relationship with FormPage
|
||||
@ManyToOne(() => FormPage, formPage => formPage.formSections)
|
||||
formPage: FormPage;
|
||||
}
|
||||
|
||||
export type FormSectionDocument = FormSection & Document;
|
||||
export const FormSectionSchema = SchemaFactory.createForClass(FormSection);
|
||||
FormSectionSchema.plugin(mongoosePaginate);
|
||||
|
||||
// Transform the output to remove the internal '_id'
|
||||
FormSectionSchema.set('toJSON', {
|
||||
transform: (doc: FormSectionDocument, ret: FormSection & { _id?: any }) => {
|
||||
delete ret._id;
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,13 @@
|
|||
// src/formSection/formSection.module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { FormSectionService } from './formSection.service';
|
||||
import { FormSectionController } from './formSection.controller';
|
||||
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';
|
||||
import { FormSection, FormSectionSchema } from './entity/formSection.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
FormSection,
|
||||
Attachment,
|
||||
Question,
|
||||
Answer,
|
||||
Option,
|
||||
MongooseModule.forFeature([
|
||||
{ name: FormSection.name, schema: FormSectionSchema },
|
||||
]),
|
||||
],
|
||||
controllers: [FormSectionController],
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { FormSection } from './entity/formSection.entity';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { FormSection, FormSectionDocument } from './entity/formSection.entity';
|
||||
import { CreateFormSectionDto } from './dto/create-formSection.dto';
|
||||
import { UpdateFormSectionDto } from './dto/update-formSection.dto';
|
||||
import { PaginateModel } from '../participant/participant.service';
|
||||
|
||||
@Injectable()
|
||||
export class FormSectionService {
|
||||
constructor(
|
||||
@InjectRepository(FormSection)
|
||||
private readonly formSectionRepo: Repository<FormSection>,
|
||||
@InjectModel(FormSection.name)
|
||||
private readonly formSectionModel: PaginateModel<FormSectionDocument>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateFormSectionDto): Promise<FormSection> {
|
||||
const formSection = this.formSectionRepo.create({
|
||||
const formSection = new this.formSectionModel({
|
||||
...data,
|
||||
id: data.id || undefined, // Let BaseEntity generate UUID if not provided
|
||||
metadata: data.metadata || { entries: [] },
|
||||
});
|
||||
return await this.formSectionRepo.save(formSection);
|
||||
return formSection.save();
|
||||
}
|
||||
|
||||
async findAll(
|
||||
|
|
@ -33,31 +36,18 @@ export class FormSectionService {
|
|||
nextPage: number | null;
|
||||
prevPage: number | null;
|
||||
}> {
|
||||
const [docs, totalDocs] = await this.formSectionRepo.findAndCount({
|
||||
skip: (page - 1) * limit,
|
||||
take: limit,
|
||||
relations: ['attachments', 'questions', 'questions.options', 'questions.answers'], // Load related data
|
||||
});
|
||||
|
||||
const totalPages = Math.ceil(totalDocs / limit);
|
||||
return {
|
||||
docs,
|
||||
totalDocs,
|
||||
limit,
|
||||
page,
|
||||
totalPages,
|
||||
hasNextPage: page < totalPages,
|
||||
hasPrevPage: page > 1,
|
||||
nextPage: page < totalPages ? page + 1 : null,
|
||||
prevPage: page > 1 ? page - 1 : null,
|
||||
};
|
||||
// Selects all fields from the FormSection entity for the response
|
||||
return this.formSectionModel.paginate(
|
||||
{},
|
||||
{ page, limit, lean: true },
|
||||
);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<FormSection | null> {
|
||||
const formSection = await this.formSectionRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['attachments', 'questions', 'questions.options', 'questions.answers'], // Load related data
|
||||
});
|
||||
const formSection = await this.formSectionModel
|
||||
.findOne({ id })
|
||||
.lean()
|
||||
.exec();
|
||||
if (!formSection) {
|
||||
throw new NotFoundException(`FormSection with ID "${id}" not found`);
|
||||
}
|
||||
|
|
@ -68,23 +58,24 @@ export class FormSectionService {
|
|||
id: string,
|
||||
data: UpdateFormSectionDto,
|
||||
): Promise<FormSection | null> {
|
||||
// TypeORM's update method returns UpdateResult, not the entity itself.
|
||||
// We update and then fetch the updated entity.
|
||||
const result = await this.formSectionRepo.update(
|
||||
{ id },
|
||||
{ ...data, updatedAt: new Date() },
|
||||
);
|
||||
const updatedFormSection = await this.formSectionModel
|
||||
.findOneAndUpdate(
|
||||
{ id },
|
||||
{ $set: { ...data, updatedAt: new Date() } },
|
||||
{ new: true },
|
||||
)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (result.affected === 0) {
|
||||
if (!updatedFormSection) {
|
||||
throw new NotFoundException(`FormSection with ID "${id}" not found`);
|
||||
}
|
||||
// Fetch and return the updated entity to ensure all fields are fresh.
|
||||
return await this.findById(id);
|
||||
return updatedFormSection;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.formSectionRepo.delete({ id });
|
||||
if (result.affected === 0) {
|
||||
const result = await this.formSectionModel.deleteOne({ id }).exec();
|
||||
if (result.deletedCount === 0) {
|
||||
throw new NotFoundException(`FormSection with ID "${id}" not found`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { AppModule } from './app.module';
|
|||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.enableCors({ origin: 'http://localhost:3001' });
|
||||
await app.listen(process.env.PORT ?? 3000);
|
||||
}
|
||||
bootstrap();
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
{
|
||||
"token": {
|
||||
"exp": 1753279286,
|
||||
"iat": 1753250486,
|
||||
"jti": "onrtro:4cee0913-cbe6-89c0-d4bb-dac3d8e29b04",
|
||||
"iss": "https://auth.didvan.com/realms/didvan",
|
||||
"aud": "account",
|
||||
"sub": "010be0e5-fe3d-4686-b093-8fe37ccdc3c9",
|
||||
"typ": "Bearer",
|
||||
"azp": "didvan-app",
|
||||
"sid": "be164904-009b-450e-98d5-553073af0fcb",
|
||||
"acr": "1",
|
||||
"allowed-origins": [
|
||||
"https://didvan.com",
|
||||
"https://web.didvan.com",
|
||||
"http://localhost:3000"
|
||||
],
|
||||
"realm_access": {
|
||||
"roles": [
|
||||
"delphi-user",
|
||||
"offline_access",
|
||||
"uma_authorization",
|
||||
"default-roles-didvan"
|
||||
]
|
||||
},
|
||||
"resource_access": {
|
||||
"account": {
|
||||
"roles": [
|
||||
"manage-account",
|
||||
"manage-account-links",
|
||||
"view-profile"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scope": "email profile",
|
||||
"email_verified": true,
|
||||
"name": "alice alice_lastname",
|
||||
"preferred_username": "alice",
|
||||
"given_name": "alice",
|
||||
"family_name": "alice_lastname",
|
||||
"email": "alice@gmail.com"
|
||||
},
|
||||
"data": {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { IsString } from 'class-validator';
|
||||
import { IsString, IsOptional, ValidateNested, IsUUID, IsEnum } from 'class-validator';
|
||||
import { BaseDto } from '../../_core/dto/base.dto';
|
||||
|
||||
export class CreateOptionDto extends BaseDto {
|
||||
// DTO for embedded Option
|
||||
export class CreateOptionDto extends BaseDto{
|
||||
@IsString()
|
||||
text: string;
|
||||
}
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import { Column, Entity, ManyToOne } from 'typeorm';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Document } from 'mongoose';
|
||||
import { BaseEntity } from 'src/_core/entity/_base.entity';
|
||||
import { Question } from 'src/question/entity/question.entity';
|
||||
|
||||
@Entity({ name: 'options' })
|
||||
@Schema({ _id: false, id: false }) // Embedded document, inheriting from BaseEntity
|
||||
export class Option extends BaseEntity {
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
text: string;
|
||||
|
||||
@ManyToOne(() => Question, (question) => question.options)
|
||||
question: Question;
|
||||
|
||||
|
||||
}
|
||||
|
||||
export type OptionDocument = Option & Document;
|
||||
export const OptionSchema = SchemaFactory.createForClass(Option);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
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 } from '@nestjs/common';
|
||||
import { ParseUUIDPipe } from '@nestjs/common';
|
||||
import { OptionService } from './option.service';
|
||||
import { CreateOptionDto } from './dto/create-option.dto';
|
||||
import { UpdateOptionDto } from './dto/update-option.dto';
|
||||
import { Option } from './entity/option.entity';
|
||||
import { Paginate, Paginated, PaginateQuery } from 'nestjs-paginate';
|
||||
|
||||
@Controller('options')
|
||||
export class OptionController {
|
||||
constructor(private readonly optionService: OptionService) {}
|
||||
|
||||
@Post()
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async create(@Body() body: CreateOptionDto): Promise<Option> {
|
||||
return this.optionService.create(body);
|
||||
}
|
||||
|
|
@ -19,25 +21,25 @@ export class OptionController {
|
|||
@Query('page') page: string = '1',
|
||||
@Query('limit') limit: string = '10',
|
||||
) {
|
||||
// The service returns the full pagination object
|
||||
return this.optionService.findAll(parseInt(page, 10), parseInt(limit, 10));
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id', new ParseUUIDPipe()) id: string): Promise<Option> {
|
||||
async findOne(@Param('id', new ParseUUIDPipe()) id: string): Promise<Option | null> {
|
||||
return this.optionService.findById(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async update(
|
||||
@Param('id', new ParseUUIDPipe()) id: string,
|
||||
@Body() body: UpdateOptionDto,
|
||||
): Promise<Option> {
|
||||
): Promise<Option | null> {
|
||||
return this.optionService.update(id, body);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> {
|
||||
return this.optionService.remove(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { OptionService } from './option.service';
|
||||
import { OptionController } from './option.controller';
|
||||
import { Option } from './entity/option.entity';
|
||||
import { Option, OptionSchema } from './entity/option.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Option]),
|
||||
MongooseModule.forFeature([{ name: Option.name, schema: OptionSchema }]),
|
||||
],
|
||||
controllers: [OptionController],
|
||||
providers: [OptionService],
|
||||
|
|
|
|||
|
|
@ -1,34 +1,23 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { CreateOptionDto } from './dto/create-option.dto';
|
||||
import { UpdateOptionDto } from './dto/update-option.dto';
|
||||
import { Option } from './entity/option.entity';
|
||||
import { Option, OptionDocument } from './entity/option.entity';
|
||||
import { PaginateModel } from '../participant/participant.service';
|
||||
|
||||
@Injectable()
|
||||
export class OptionService {
|
||||
constructor(
|
||||
@InjectRepository(Option)
|
||||
private readonly optionRepository: Repository<Option>,
|
||||
@InjectModel(Option.name)
|
||||
private readonly optionModel: PaginateModel<OptionDocument>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateOptionDto): Promise<Option> {
|
||||
try {
|
||||
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}`);
|
||||
}
|
||||
const option = new this.optionModel({ ...data, id: require('uuid').v4() });
|
||||
return option.save();
|
||||
}
|
||||
|
||||
async findAll(
|
||||
page = 1,
|
||||
limit = 10,
|
||||
): Promise<{
|
||||
async findAll(page = 1, limit = 10): Promise<{
|
||||
docs: Option[];
|
||||
totalDocs: number;
|
||||
limit: number;
|
||||
|
|
@ -39,49 +28,25 @@ export class OptionService {
|
|||
nextPage: number | null;
|
||||
prevPage: number | null;
|
||||
}> {
|
||||
try {
|
||||
const skip = (page - 1) * limit;
|
||||
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}`);
|
||||
}
|
||||
return this.optionModel.paginate(
|
||||
{},
|
||||
{ page, limit, lean: true },
|
||||
);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Option> {
|
||||
const option = await this.optionRepository.findOne({ where: { id } });
|
||||
if (!option) {
|
||||
throw new NotFoundException(`Option with ID "${id}" not found`);
|
||||
}
|
||||
return option;
|
||||
|
||||
async findById(id: string): Promise<Option | null> {
|
||||
return this.optionModel.findOne({ id }).lean().exec();
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateOptionDto): Promise<Option> {
|
||||
const result = await this.optionRepository.update({ id }, { ...data, updatedAt: new Date() });
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Option with ID "${id}" not found`);
|
||||
}
|
||||
return await this.findById(id);
|
||||
async update(id: string, data: UpdateOptionDto): Promise<Option | null> {
|
||||
return this.optionModel
|
||||
.findOneAndUpdate({ id }, { ...data, updatedAt: new Date() }, { new: true })
|
||||
.lean()
|
||||
.exec();
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.optionRepository.delete({ id });
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Option with ID "${id}" not found`);
|
||||
}
|
||||
await this.optionModel.deleteOne({ id }).exec();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,15 @@ import { Type } from 'class-transformer';
|
|||
import { MetadataDto } from '../../_core/dto/metadataEntry.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()
|
||||
displayName: string;
|
||||
displayName: string; // Required, provided by client
|
||||
|
||||
@IsEnum(['moderator', 'tester', 'admin', 'user'])
|
||||
@IsOptional()
|
||||
role?: 'moderator' | 'tester' | 'admin' | 'user';
|
||||
role?: 'moderator' | 'tester' | 'admin' | 'user'; // Optional, defaults to 'user'
|
||||
|
||||
}
|
||||
|
|
@ -1,31 +1,40 @@
|
|||
import { Column, Entity, ManyToOne } from 'typeorm';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { BaseEntity } from 'src/_core/entity/_base.entity';
|
||||
import { Form } from 'src/form/entity/form.entity';
|
||||
import { Realm } from '../../realm/entity/realm.entity';
|
||||
import * as mongoosePaginate from 'mongoose-paginate-v2';
|
||||
import { Document } from 'mongoose';
|
||||
|
||||
@Entity({ name: 'participants' })
|
||||
@Schema({ _id: false, id: false }) // Disable _id and virtual id
|
||||
export class Participant extends BaseEntity {
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
unique: true,
|
||||
@Prop({
|
||||
type: String,
|
||||
default: () => uuidv4(),
|
||||
required: true,
|
||||
immutable: true,
|
||||
})
|
||||
userId: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
@Prop({
|
||||
type: String,
|
||||
enum: ['moderator', 'tester', 'admin', 'user'],
|
||||
default: 'user',
|
||||
required: true,
|
||||
})
|
||||
role: 'moderator' | 'tester' | 'admin' | 'user';
|
||||
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
displayName: string;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
export type ParticipantDocument = Participant & Document;
|
||||
export const ParticipantSchema = SchemaFactory.createForClass(Participant);
|
||||
ParticipantSchema.plugin(mongoosePaginate);
|
||||
ParticipantSchema.set('toJSON', {
|
||||
transform: (doc: ParticipantDocument, ret: Participant & { _id?: any }) => {
|
||||
delete ret._id;
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
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 } from '@nestjs/common';
|
||||
import { ParticipantService } from './participant.service';
|
||||
import { CreateParticipantDto } from './dto/create-participant.dto';
|
||||
import { UpdateParticipantDto } from './dto/update-participant.dto';
|
||||
|
|
@ -14,7 +14,7 @@ export class ParticipantController {
|
|||
return this.participantService.create(body);
|
||||
}
|
||||
|
||||
@Get('findAll')
|
||||
@Get('findAll') // returns non uuid id !!!
|
||||
async findAll(
|
||||
@Query('page') page: string = '1',
|
||||
@Query('limit') limit: string = '10',
|
||||
|
|
@ -32,19 +32,22 @@ export class ParticipantController {
|
|||
return this.participantService.findAll(parseInt(page), parseInt(limit));
|
||||
}
|
||||
|
||||
@Get(':userId')
|
||||
async findOne(@Param('userId') userId: string): Promise<Participant | null> {
|
||||
return this.participantService.findByUserId(userId);
|
||||
@Get(':id')
|
||||
async findOne(@Param('id', new ParseUUIDPipe()) id: string): Promise<Participant | null> {
|
||||
return this.participantService.findById(id);
|
||||
}
|
||||
|
||||
@Patch(':userId')
|
||||
@Patch(':id')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async update(@Param('userId') userId: string, @Body() body: UpdateParticipantDto): Promise<Participant | null> {
|
||||
return this.participantService.update(userId, body);
|
||||
async update(
|
||||
@Param('id', new ParseUUIDPipe()) id: string,
|
||||
@Body() body: UpdateParticipantDto,
|
||||
): Promise<Participant | null> {
|
||||
return this.participantService.update(id, body);
|
||||
}
|
||||
|
||||
@Delete(':userId')
|
||||
async remove(@Param('userId') userId: string): Promise<void> {
|
||||
return this.participantService.remove(userId);
|
||||
@Delete(':id')
|
||||
async remove(@Param('id', new ParseUUIDPipe()) id: string): Promise<void> {
|
||||
return this.participantService.remove(id);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { ParticipantService } from './participant.service';
|
||||
import { ParticipantController } from './participant.controller';
|
||||
import { Participant } from './entity/participant.entity';
|
||||
import { Participant, ParticipantSchema } from './entity/participant.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([Participant]),
|
||||
MongooseModule.forFeature([{ name: Participant.name, schema: ParticipantSchema }]),
|
||||
],
|
||||
controllers: [ParticipantController],
|
||||
providers: [ParticipantService],
|
||||
|
|
|
|||
|
|
@ -1,69 +1,45 @@
|
|||
import { Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { CreateParticipantDto } from './dto/create-participant.dto';
|
||||
import { Participant } from './entity/participant.entity';
|
||||
import { Participant, ParticipantDocument } from './entity/participant.entity';
|
||||
import { UpdateParticipantDto } from './dto/update-participant.dto';
|
||||
import axios from 'axios';
|
||||
import { decode } from 'jsonwebtoken';
|
||||
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()
|
||||
export class ParticipantService {
|
||||
constructor(
|
||||
@InjectRepository(Participant)
|
||||
private readonly participantRepository: Repository<Participant>,
|
||||
@InjectModel(Participant.name)
|
||||
private readonly participantModel: PaginateModel<ParticipantDocument>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateParticipantDto): Promise<Participant> {
|
||||
try {
|
||||
// Here we send a sample request, in final implementation, there'll be no need for that, we'll get the decoded token
|
||||
const authResponse = await axios.post(
|
||||
'https://auth.didvan.com/realms/didvan/protocol/openid-connect/token',
|
||||
new URLSearchParams({
|
||||
client_id: 'didvan-app',
|
||||
username: 'bob',
|
||||
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');
|
||||
}
|
||||
|
||||
// Check if participant with this sub already exists
|
||||
const existingParticipant = await this.participantRepository.findOne({ where: { id: decodedToken.sub } });
|
||||
if (existingParticipant) {
|
||||
throw new InternalServerErrorException(`Participant with ID ${decodedToken.sub} already exists`);
|
||||
}
|
||||
|
||||
// Create the participant with the 'sub' as the ID
|
||||
const participant = this.participantRepository.create({
|
||||
...data,
|
||||
// id: , let it assign a random id
|
||||
userId: decodedToken.sub, // Set userId to the same sub
|
||||
metadata: data.metadata
|
||||
? { entries: data.metadata.entries }
|
||||
: { entries: [] },
|
||||
status: data.status || 'active',
|
||||
});
|
||||
|
||||
// Save the participant to the database
|
||||
return await this.participantRepository.save(participant);
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(`Failed to create participant: ${error.message}`);
|
||||
}
|
||||
const participant = new this.participantModel({
|
||||
...data,
|
||||
id: data.id || undefined, // Let BaseEntity generate UUID
|
||||
metadata: data.metadata || { entries: [] },
|
||||
});
|
||||
return participant.save();
|
||||
}
|
||||
|
||||
async findAll(page = 1, limit = 10): Promise<{
|
||||
async findAll(page = 1, limit = 10): Promise<{ // returns non uuid id !!!
|
||||
docs: Participant[];
|
||||
totalDocs: number;
|
||||
limit: number;
|
||||
|
|
@ -74,51 +50,26 @@ export class ParticipantService {
|
|||
nextPage: number | null;
|
||||
prevPage: number | null;
|
||||
}> {
|
||||
try {
|
||||
const skip = (page - 1) * limit;
|
||||
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}`);
|
||||
}
|
||||
return this.participantModel.paginate(
|
||||
{},
|
||||
{ page, limit, lean: true},
|
||||
);
|
||||
}
|
||||
|
||||
async findByUserId(userId: string): Promise<Participant | null> {
|
||||
try {
|
||||
return await this.participantRepository.findOne({ where: { userId } });
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(`Failed to find participant: ${error.message}`);
|
||||
}
|
||||
async findById(id: string): Promise<Participant | null> {
|
||||
return this.participantModel.findOne({ id }).lean().exec();
|
||||
}
|
||||
|
||||
async update(userId: string, data: UpdateParticipantDto): Promise<Participant | null> {
|
||||
try {
|
||||
await this.participantRepository.update({ userId }, { ...data, updatedAt: new Date() });
|
||||
return await this.participantRepository.findOne({ where: { userId } });
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(`Failed to update participant: ${error.message}`);
|
||||
}
|
||||
async update(id: string, data: UpdateParticipantDto): Promise<Participant | null> {
|
||||
return this.participantModel
|
||||
.findOneAndUpdate(
|
||||
{ id },
|
||||
{ $set: { ...data, updatedAt: new Date() } },
|
||||
{ new: true },
|
||||
).lean().exec();
|
||||
}
|
||||
|
||||
async remove(userId: string): Promise<void> {
|
||||
try {
|
||||
await this.participantRepository.delete({ userId });
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(`Failed to delete participant: ${error.message}`);
|
||||
}
|
||||
async remove(id: string): Promise<void> {
|
||||
await this.participantModel.deleteOne({ id }).exec();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,31 @@
|
|||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { BaseEntity } from '../../_core/entity/_base.entity';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { BaseEntity } from 'src/_core/entity/_base.entity';
|
||||
import * as mongoosePaginate from 'mongoose-paginate-v2';
|
||||
import { Document } from 'mongoose';
|
||||
|
||||
@Entity()
|
||||
@Schema({ _id: false, id: false }) // Disable _id and virtual id
|
||||
export class ParticipantGroup extends BaseEntity {
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
title: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['confidential', 'delphi', 'one-time-question', 'one-time-section', 'one-time-page', 'one-time-form'],
|
||||
@Prop({
|
||||
type: String,
|
||||
enum: ['confidential','delphi','one-time-question','one-time-section','one-time-page','one-time-form'],
|
||||
// default: '',
|
||||
required: true,
|
||||
})
|
||||
type: 'confidential' | 'delphi' | 'one-time-question' | 'one-time-section' | 'one-time-page' | 'one-time-form';
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ParticipantGroup } from './entity/participantGroup.entity';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { ParticipantGroupService } from './participantGroup.service';
|
||||
import { ParticipantGroupController } from './participantGroup.controller';
|
||||
import { ParticipantGroup, ParticipantGroupSchema } from './entity/participantGroup.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ParticipantGroup])],
|
||||
imports: [
|
||||
MongooseModule.forFeature([{ name: ParticipantGroup.name, schema: ParticipantGroupSchema }]),
|
||||
],
|
||||
controllers: [ParticipantGroupController],
|
||||
providers: [ParticipantGroupService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,64 +1,60 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ParticipantGroup } from './entity/participantGroup.entity';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { CreateParticipantGroupDto } from './dto/create-participantGroup.dto';
|
||||
import { ParticipantGroup, ParticipantGroupDocument } from './entity/participantGroup.entity';
|
||||
import { UpdateParticipantGroupDto } from './dto/update-participantGroup.dto';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { PaginateModel } from '../participant/participant.service';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class ParticipantGroupService {
|
||||
constructor(
|
||||
@InjectRepository(ParticipantGroup)
|
||||
private readonly participantGroupRepo: Repository<ParticipantGroup>,
|
||||
@InjectModel(ParticipantGroup.name)
|
||||
private readonly participantGroupModel: PaginateModel<ParticipantGroupDocument>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateParticipantGroupDto): Promise<ParticipantGroup> {
|
||||
const group = this.participantGroupRepo.create({
|
||||
const participantGroup = new this.participantGroupModel({
|
||||
...data,
|
||||
id: data.id || undefined, // Let BaseEntity generate UUID
|
||||
metadata: data.metadata || { entries: [] },
|
||||
});
|
||||
return await this.participantGroupRepo.save(group);
|
||||
return participantGroup.save();
|
||||
}
|
||||
|
||||
async findAll(page = 1, limit = 10) {
|
||||
const [docs, totalDocs] = await this.participantGroupRepo.findAndCount({
|
||||
skip: (page - 1) * limit,
|
||||
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,
|
||||
};
|
||||
async findAll(page = 1, limit = 10): Promise<{ // returns non uuid id !!!
|
||||
docs: ParticipantGroup[];
|
||||
totalDocs: number;
|
||||
limit: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
hasNextPage: boolean;
|
||||
hasPrevPage: boolean;
|
||||
nextPage: number | null;
|
||||
prevPage: number | null;
|
||||
}> {
|
||||
return this.participantGroupModel.paginate(
|
||||
{},
|
||||
{ page, limit, lean: true },
|
||||
);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<ParticipantGroup> {
|
||||
const group = await this.participantGroupRepo.findOne({ where: { id } });
|
||||
if (!group) {
|
||||
throw new NotFoundException(`Participant group with ID "${id}" not found`);
|
||||
}
|
||||
return group;
|
||||
async findById(id: string): Promise<ParticipantGroup | null> {
|
||||
return this.participantGroupModel.findOne({ id }).lean().exec();
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateParticipantGroupDto): Promise<ParticipantGroup> {
|
||||
const result = await this.participantGroupRepo.update({ id }, { ...data, updatedAt: new Date() });
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Participant group with ID "${id}" not found`);
|
||||
}
|
||||
return await this.findById(id);
|
||||
async update(id: string, data: UpdateParticipantGroupDto): Promise<ParticipantGroup | null> {
|
||||
return this.participantGroupModel
|
||||
.findOneAndUpdate(
|
||||
{ id },
|
||||
{ $set: { ...data, updatedAt: new Date() } },
|
||||
{ new: true },
|
||||
).lean().exec();
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.participantGroupRepo.delete({ id });
|
||||
if (result.affected === 0) {
|
||||
throw new NotFoundException(`Participant group with ID "${id}" not found`);
|
||||
}
|
||||
await this.participantGroupModel.deleteOne({ id }).exec();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,16 @@
|
|||
import {
|
||||
IsString,
|
||||
IsBoolean,
|
||||
IsOptional,
|
||||
IsArray,
|
||||
IsEnum,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { IsString, IsBoolean, IsOptional, IsUUID, ValidateNested, IsArray, IsEnum, IsUrl,} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import {CreateAttachmentDto} from '../../attachment/dto/create_attachment.dto';
|
||||
import { BaseDto } from '../../_core/dto/base.dto';
|
||||
import { CreateOptionDto } from '../../option/dto/create-option.dto';
|
||||
import { CreateAttachmentDto } from '../../attachment/dto/create_attachment.dto';
|
||||
|
||||
export class CreateQuestionDto extends BaseDto {
|
||||
// Main DTO for creating a Question
|
||||
export class CreateQuestionDto extends BaseDto{
|
||||
@IsString()
|
||||
text: string;
|
||||
|
||||
@IsEnum(['multiple-choice', 'single-choice', 'text'])
|
||||
type: 'multiple-choice' | 'single-choice' | 'text';
|
||||
@IsEnum(['multiple-choice', 'single-choice', 'text', 'rating', 'file'])
|
||||
type: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
|
|
|
|||
|
|
@ -1,49 +1,67 @@
|
|||
import { Column, Entity, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
||||
import { Document } from 'mongoose';
|
||||
import { BaseEntity } from 'src/_core/entity/_base.entity';
|
||||
import { Attachment } from '../../attachment/entity/attachment.entity';
|
||||
import { Option } from '../../option/entity/option.entity';
|
||||
import { Answer } from '../../answer/entity/answer.entity';
|
||||
import { FormSection } from '../../formSection/entity/formSection.entity';
|
||||
import * as mongoosePaginate from 'mongoose-paginate-v2';
|
||||
import { Attachment, AttachmentSchema } from '../../attachment/entity/attachment.entity';
|
||||
import { Option, OptionSchema } from '../../option/entity/option.entity';
|
||||
|
||||
@Entity({ name: 'questions' })
|
||||
@Schema({ _id: false, id: false }) // Disable _id and virtual id
|
||||
export class Question extends BaseEntity {
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
text: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
enum: ['multiple-choice', 'single-choice', 'text'],
|
||||
default: 'text',
|
||||
})
|
||||
type: 'multiple-choice' | 'single-choice' | 'text';
|
||||
|
||||
@Column({
|
||||
type: 'boolean',
|
||||
@Prop({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true,
|
||||
})
|
||||
isRequired: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'boolean',
|
||||
@Prop({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true,
|
||||
})
|
||||
isConfidential: boolean;
|
||||
isConfidential: boolean; // no real-time updates for normal users
|
||||
|
||||
@Column({
|
||||
type: 'varchar',
|
||||
nullable: true,
|
||||
@Prop({
|
||||
type: String,
|
||||
required: false, // Assuming validation rules might not always be present
|
||||
})
|
||||
validationRules?: string;
|
||||
|
||||
@OneToMany(() => Option, (option) => option.question, { cascade: true })
|
||||
@Prop({
|
||||
type: [OptionSchema],
|
||||
default: [],
|
||||
})
|
||||
options: Option[];
|
||||
|
||||
@OneToMany(() => Attachment, (attachment) => attachment.question, { cascade: true })
|
||||
@Prop({
|
||||
type: [AttachmentSchema],
|
||||
default: [],
|
||||
})
|
||||
attachments: Attachment[];
|
||||
|
||||
@OneToMany(() => Answer, answer => answer.question, { cascade: true })
|
||||
answers: Answer[]; // Answers to this question
|
||||
|
||||
@ManyToOne(() => FormSection, formSection => formSection.questions)
|
||||
formSection: FormSection;
|
||||
}
|
||||
|
||||
export type QuestionDocument = Question & Document;
|
||||
export const QuestionSchema = SchemaFactory.createForClass(Question);
|
||||
QuestionSchema.plugin(mongoosePaginate);
|
||||
|
||||
// Transform the output to remove the internal '_id'
|
||||
QuestionSchema.set('toJSON', {
|
||||
transform: (doc: QuestionDocument, ret: Question & { _id?: any }) => {
|
||||
delete ret._id;
|
||||
return ret;
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Question } from './entity/question.entity';
|
||||
import { QuestionController } from './question.controller';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { QuestionService } from './question.service';
|
||||
import { QuestionController } from './question.controller';
|
||||
import { Question, QuestionSchema } from './entity/question.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Question])],
|
||||
imports: [
|
||||
MongooseModule.forFeature([
|
||||
{ name: Question.name, schema: QuestionSchema },
|
||||
]),
|
||||
],
|
||||
controllers: [QuestionController],
|
||||
providers: [QuestionService],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,58 +1,81 @@
|
|||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Question } from './entity/question.entity';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { Question, QuestionDocument } from './entity/question.entity';
|
||||
import { CreateQuestionDto } from './dto/create-question.dto';
|
||||
import { UpdateQuestionDto } from './dto/update-question.dto';
|
||||
import { PaginateModel } from '../participant/participant.service';
|
||||
|
||||
@Injectable()
|
||||
export class QuestionService {
|
||||
constructor(
|
||||
@InjectRepository(Question)
|
||||
private readonly questionRepo: Repository<Question>,
|
||||
@InjectModel(Question.name)
|
||||
private readonly questionModel: PaginateModel<QuestionDocument>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateQuestionDto): Promise<Question> {
|
||||
const question = this.questionRepo.create(data);
|
||||
return this.questionRepo.save(question);
|
||||
const question = new this.questionModel({
|
||||
...data,
|
||||
id: data.id || undefined, // Let BaseEntity generate UUID if not provided
|
||||
metadata: data.metadata || { entries: [] },
|
||||
});
|
||||
return question.save();
|
||||
}
|
||||
|
||||
async findAll(page = 1, limit = 10) {
|
||||
const [docs, totalDocs] = await this.questionRepo.findAndCount({
|
||||
skip: (page - 1) * limit,
|
||||
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,
|
||||
};
|
||||
async findAll(
|
||||
page = 1,
|
||||
limit = 10,
|
||||
): Promise<{
|
||||
docs: Question[];
|
||||
totalDocs: number;
|
||||
limit: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
hasNextPage: boolean;
|
||||
hasPrevPage: boolean;
|
||||
nextPage: number | null;
|
||||
prevPage: number | null;
|
||||
}> {
|
||||
// Selects all fields from the Question entity for the response
|
||||
return this.questionModel.paginate(
|
||||
{},
|
||||
{ page, limit, lean: true },
|
||||
);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Question | null> {
|
||||
const question = await this.questionRepo.findOneBy({ id });
|
||||
const question = await this.questionModel
|
||||
.findOne({ id })
|
||||
.lean()
|
||||
.exec();
|
||||
if (!question) {
|
||||
throw new NotFoundException(`Question with ID "${id}" not found`);
|
||||
}
|
||||
return question;
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateQuestionDto): Promise<Question | null> {
|
||||
await this.questionRepo.update(id, data);
|
||||
return this.findById(id);
|
||||
async update(
|
||||
id: string,
|
||||
data: UpdateQuestionDto,
|
||||
): 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> {
|
||||
const result = await this.questionRepo.delete(id);
|
||||
if (result.affected === 0) {
|
||||
const result = await this.questionModel.deleteOne({ id }).exec();
|
||||
if (result.deletedCount === 0) {
|
||||
throw new NotFoundException(`Question with ID "${id}" not found`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { IsString, IsOptional, ValidateNested, IsArray } from 'class-validator';
|
||||
import { IsString, IsOptional, IsUUID, ValidateNested, IsArray, IsEnum, IsDateString } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { BaseDto } from '../../_core/dto/base.dto';
|
||||
import { CreateFormDto } from '../../form/dto/create-form.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';
|
||||
|
||||
export class CreateRealmDto extends BaseDto {
|
||||
@IsString()
|
||||
|
|
@ -20,10 +23,10 @@ export class CreateRealmDto extends BaseDto {
|
|||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateParticipantDto)
|
||||
@IsOptional()
|
||||
participants?: CreateParticipantDto[];
|
||||
participant: CreateParticipantDto[];
|
||||
|
||||
// @ValidateNested() // Not IsArray, as it's a single owner
|
||||
// @Type(() => CreateParticipantDto)
|
||||
// owner: CreateParticipantDto;
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateParticipantDto)
|
||||
owner: CreateParticipantDto;
|
||||
}
|
||||
|
|
@ -1,24 +1,58 @@
|
|||
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';
|
||||
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';
|
||||
|
||||
@Entity()
|
||||
@Schema({ _id: false, id: false }) // Disable _id and virtual id
|
||||
export class Realm extends BaseEntity {
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
title: string;
|
||||
|
||||
@Column()
|
||||
@Prop({
|
||||
type: String,
|
||||
required: true,
|
||||
})
|
||||
description: string;
|
||||
|
||||
@OneToMany(() => Form, form => form.realm, { cascade: true, eager: true })
|
||||
@Prop({
|
||||
type: [FormSchema],
|
||||
default: [],
|
||||
})
|
||||
forms: Form[];
|
||||
|
||||
@OneToMany(() => Participant, participant => participant.realm, { cascade: true, eager: true })
|
||||
@Prop({
|
||||
type: [ParticipantSchema],
|
||||
default: [],
|
||||
})
|
||||
participants: Participant[];
|
||||
|
||||
@OneToOne(() => Participant, { cascade: true, eager: true })
|
||||
@JoinColumn({ name: 'ownerId', referencedColumnName: 'userId' }) // be explicit
|
||||
@Prop({
|
||||
type: ParticipantSchema,
|
||||
})
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ 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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { MongooseModule } from '@nestjs/mongoose';
|
||||
import { RealmService } from './realm.service';
|
||||
import { RealmController } from './realm.controller';
|
||||
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';
|
||||
import { Realm, RealmSchema } from './entity/realm.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
Realm,
|
||||
Form,
|
||||
FormPage,
|
||||
FormSection,
|
||||
Attachment,
|
||||
Question,
|
||||
Answer,
|
||||
Option,
|
||||
Participant,
|
||||
MongooseModule.forFeature([
|
||||
{ name: Realm.name, schema: RealmSchema },
|
||||
]),
|
||||
],
|
||||
controllers: [RealmController],
|
||||
|
|
|
|||
|
|
@ -1,73 +1,27 @@
|
|||
import { Injectable, NotFoundException, InternalServerErrorException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Realm } from './entity/realm.entity';
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectModel } from '@nestjs/mongoose';
|
||||
import { Model } from 'mongoose';
|
||||
import { Realm, RealmDocument } from './entity/realm.entity';
|
||||
import { CreateRealmDto } from './dto/create-realm.dto';
|
||||
import { UpdateRealmDto } from './dto/update-realm.dto';
|
||||
import { Participant } from '../participant/entity/participant.entity';
|
||||
import axios from 'axios';
|
||||
import { decode } from 'jsonwebtoken';
|
||||
import { PaginateModel } from '../participant/participant.service';
|
||||
|
||||
@Injectable()
|
||||
export class RealmService {
|
||||
constructor(
|
||||
@InjectRepository(Realm)
|
||||
private readonly realmRepo: Repository<Realm>,
|
||||
@InjectRepository(Participant)
|
||||
private readonly participantRepo: Repository<Participant>,
|
||||
@InjectModel(Realm.name)
|
||||
private readonly realmModel: PaginateModel<RealmDocument>,
|
||||
) {}
|
||||
|
||||
async create(data: CreateRealmDto): Promise<Realm> {
|
||||
try {
|
||||
// Here we send a sample request, in final implementation, there'll be no need for that, we'll get the decoded token
|
||||
const authResponse = await axios.post(
|
||||
'https://auth.didvan.com/realms/didvan/protocol/openid-connect/token',
|
||||
new URLSearchParams({
|
||||
client_id: 'didvan-app',
|
||||
username: 'bob',
|
||||
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');
|
||||
}
|
||||
|
||||
// Find or create a participant with userId set to sub
|
||||
let participant = await this.participantRepo.findOne({ where: { userId: decodedToken.sub } });
|
||||
if (!participant) {
|
||||
participant = this.participantRepo.create({
|
||||
displayName: data.title + ' Owner', // Default displayName
|
||||
role: 'admin', // Default role for owner
|
||||
userId: decodedToken.sub,
|
||||
metadata: data.metadata ? { entries: data.metadata.entries } : { entries: [] },
|
||||
// status: 'active',
|
||||
});
|
||||
participant = await this.participantRepo.save(participant);
|
||||
}
|
||||
|
||||
// Create the realm with the participant as the owner
|
||||
const realm = this.realmRepo.create({
|
||||
...data,
|
||||
owner: participant, // Set owner to the participant with userId = sub
|
||||
});
|
||||
|
||||
return await this.realmRepo.save(realm);
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException(`Failed to create realm: ${error.message}`);
|
||||
}
|
||||
const realm = new this.realmModel({
|
||||
...data,
|
||||
id: data.id || undefined, // Let BaseEntity generate UUID if not provided
|
||||
metadata: data.metadata || { entries: [] },
|
||||
});
|
||||
return realm.save();
|
||||
}
|
||||
|
||||
// Other methods (findAll, findById, update, remove) remain unchanged
|
||||
async findAll(
|
||||
page = 1,
|
||||
limit = 10,
|
||||
|
|
@ -82,72 +36,46 @@ export class RealmService {
|
|||
nextPage: number | null;
|
||||
prevPage: number | null;
|
||||
}> {
|
||||
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,
|
||||
};
|
||||
// Selects all fields from the Realm entity for the response
|
||||
return this.realmModel.paginate(
|
||||
{},
|
||||
{ page, limit, lean: true },
|
||||
);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<Realm | null> {
|
||||
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',
|
||||
],
|
||||
});
|
||||
const realm = await this.realmModel
|
||||
.findOne({ id })
|
||||
.lean()
|
||||
.exec();
|
||||
if (!realm) {
|
||||
throw new NotFoundException(`Realm with ID "${id}" not found`);
|
||||
}
|
||||
return realm;
|
||||
}
|
||||
|
||||
async update(id: string, data: UpdateRealmDto): Promise<Realm | null> {
|
||||
const result = await this.realmRepo.update(
|
||||
{ id },
|
||||
{ ...data, updatedAt: new Date() },
|
||||
);
|
||||
async update(
|
||||
id: string,
|
||||
data: UpdateRealmDto,
|
||||
): Promise<Realm | null> {
|
||||
const updatedRealm = await this.realmModel
|
||||
.findOneAndUpdate(
|
||||
{ id },
|
||||
{ $set: { ...data, updatedAt: new Date() } },
|
||||
{ new: true },
|
||||
)
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (result.affected === 0) {
|
||||
if (!updatedRealm) {
|
||||
throw new NotFoundException(`Realm with ID "${id}" not found`);
|
||||
}
|
||||
return await this.findById(id);
|
||||
return updatedRealm;
|
||||
}
|
||||
|
||||
async remove(id: string): Promise<void> {
|
||||
const result = await this.realmRepo.delete({ id });
|
||||
if (result.affected === 0) {
|
||||
const result = await this.realmModel.deleteOne({ id }).exec();
|
||||
if (result.deletedCount === 0) {
|
||||
throw new NotFoundException(`Realm with ID "${id}" not found`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue