used bullmq in formResult
This commit is contained in:
parent
a5f76f01ad
commit
9af4f9dcf8
|
|
@ -9,6 +9,7 @@
|
|||
"version": "0.0.1",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@nestjs/bullmq": "^11.0.3",
|
||||
"@nestjs/common": "^11.1.3",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.3",
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
"@nestjs/mongoose": "^11.0.3",
|
||||
"@nestjs/platform-express": "^11.1.3",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"bullmq": "^5.56.7",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"glob": "^11.0.3",
|
||||
|
|
@ -1380,6 +1382,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@ioredis/commands": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz",
|
||||
"integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
|
|
@ -2043,6 +2050,78 @@
|
|||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
|
||||
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@napi-rs/nice": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.4.tgz",
|
||||
|
|
@ -2343,6 +2422,32 @@
|
|||
"@tybys/wasm-util": "^0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/bull-shared": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-11.0.3.tgz",
|
||||
"integrity": "sha512-CaHniPkLAxis6fAB1DB8WZELQv8VPCLedbj7iP0VQ1pz74i6NSzG9mBg6tOomXq/WW4la4P4OMGEQ48UAJh20A==",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^10.0.0 || ^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/bullmq": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-11.0.3.tgz",
|
||||
"integrity": "sha512-0Qr7Fk3Ir3V2OBIKJk+ArEM0AesGjKaNZA8QQ4fH3qGogudYADSjaNe910/OAfmX8q+cjCRorvwTLdcShwWEMw==",
|
||||
"dependencies": {
|
||||
"@nestjs/bull-shared": "^11.0.3",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
||||
"bullmq": "^3.0.0 || ^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/cli": {
|
||||
"version": "11.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.7.tgz",
|
||||
|
|
@ -5500,6 +5605,32 @@
|
|||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "5.56.7",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.56.7.tgz",
|
||||
"integrity": "sha512-Aa4Y7rmkuZOuyHvitGd2rSETgQ0SMawPD0XVsU9gAUg9Q4bwNpSD9ZDVcWGhfAQAUZFJH82JUmLsm0B59Prmtg==",
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.9.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"msgpackr": "^1.11.2",
|
||||
"node-abort-controller": "^3.1.1",
|
||||
"semver": "^7.5.4",
|
||||
"tslib": "^2.0.0",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bullmq/node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
|
|
@ -5825,6 +5956,14 @@
|
|||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
|
|
@ -6029,6 +6168,17 @@
|
|||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/cron-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
|
||||
"dependencies": {
|
||||
"luxon": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
|
@ -6166,6 +6316,14 @@
|
|||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
|
|
@ -6183,6 +6341,15 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-newline": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz",
|
||||
|
|
@ -7784,6 +7951,29 @@
|
|||
"kind-of": "^6.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/ioredis": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz",
|
||||
"integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==",
|
||||
"dependencies": {
|
||||
"@ioredis/commands": "^1.1.1",
|
||||
"cluster-key-slot": "^1.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"denque": "^2.1.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.isarguments": "^3.1.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0",
|
||||
"standard-as-callback": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ioredis"
|
||||
}
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
|
|
@ -9064,11 +9254,21 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
|
||||
},
|
||||
"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.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
|
|
@ -9148,6 +9348,14 @@
|
|||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz",
|
||||
"integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
|
|
@ -9503,6 +9711,35 @@
|
|||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "1.11.5",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz",
|
||||
"integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==",
|
||||
"optionalDependencies": {
|
||||
"msgpackr-extract": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/msgpackr-extract": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
|
||||
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build-optional-packages": "5.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/multer": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
|
||||
|
|
@ -9621,8 +9858,7 @@
|
|||
"node_modules/node-abort-controller": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
|
||||
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="
|
||||
},
|
||||
"node_modules/node-emoji": {
|
||||
"version": "1.11.0",
|
||||
|
|
@ -9633,6 +9869,20 @@
|
|||
"lodash": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build-optional-packages": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
|
||||
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-gyp-build-optional-packages": "bin.js",
|
||||
"node-gyp-build-optional-packages-optional": "optional.js",
|
||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
|
|
@ -10495,6 +10745,25 @@
|
|||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
"integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-parser": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
|
||||
"integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==",
|
||||
"dependencies": {
|
||||
"redis-errors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect-metadata": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||
|
|
@ -11135,6 +11404,11 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/standard-as-callback": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/bullmq": "^11.0.3",
|
||||
"@nestjs/common": "^11.1.3",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.1.3",
|
||||
|
|
@ -27,6 +28,7 @@
|
|||
"@nestjs/mongoose": "^11.0.3",
|
||||
"@nestjs/platform-express": "^11.1.3",
|
||||
"@nestjs/typeorm": "^11.0.0",
|
||||
"bullmq": "^5.56.7",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"glob": "^11.0.3",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { ParticipanGrouptModule } from './participantGroup/participantGroup.modu
|
|||
import { RealmModule } from './realm/realm.module';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { typeOrmConfig } from './config/database.config';
|
||||
import { FormResultWorkerModule } from './formResult/formResultWorker.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
|
|
@ -32,7 +33,8 @@ import { typeOrmConfig } from './config/database.config';
|
|||
FormSectionModule,
|
||||
OptionModule,
|
||||
ParticipanGrouptModule,
|
||||
RealmModule
|
||||
RealmModule,
|
||||
FormResultWorkerModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
|
|
|||
|
|
@ -25,8 +25,4 @@ export class CreateFormDto extends BaseDto {
|
|||
@IsArray()
|
||||
@IsUUID('4', { each: true })
|
||||
attachmentIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsUUID('4')
|
||||
formResultId?: string;
|
||||
}
|
||||
|
|
@ -20,7 +20,4 @@ export class Form extends BaseEntity {
|
|||
|
||||
@Column('simple-array', { nullable: true })
|
||||
attachmentIds: string[];
|
||||
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
formResultId: string;
|
||||
}
|
||||
|
|
@ -4,18 +4,27 @@ import { Repository } from 'typeorm';
|
|||
import { Form } from './entity/form.entity';
|
||||
import { CreateFormDto } from './dto/create-form.dto';
|
||||
import { UpdateFormDto } from './dto/update-form.dto';
|
||||
import { FormResultService } from '../formResult/formResult.service';
|
||||
|
||||
@Injectable()
|
||||
export class FormService {
|
||||
constructor(
|
||||
@InjectRepository(Form)
|
||||
private readonly formRepo: Repository<Form>,
|
||||
private readonly formResultService: FormResultService
|
||||
) {}
|
||||
|
||||
async create(data: CreateFormDto): Promise<Form> {
|
||||
const form = this.formRepo.create({
|
||||
...data,
|
||||
});
|
||||
await this.formResultService.create({
|
||||
numParticipants: 0,
|
||||
numQuestions: 0,
|
||||
numAnswers: 0,
|
||||
numComplete: 0,
|
||||
opinions: [],
|
||||
}); // Create corresponding FormResult instance and schedule statistics
|
||||
return await this.formRepo.save(form);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { IsNumber, IsString, IsOptional, IsUUID, ValidateNested, IsArray, IsDateString } from 'class-validator';
|
||||
import { IsNumber, IsString, IsUUID, ValidateNested, IsArray } from 'class-validator';
|
||||
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,17 +11,18 @@ class CreateOptionsDto {
|
|||
count: number;
|
||||
}
|
||||
|
||||
// DTO for Opinion (remains the same)
|
||||
// DTO for Opinion
|
||||
class CreateOpinionDto {
|
||||
@ValidateNested()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateOptionsDto)
|
||||
options: CreateOptionsDto;
|
||||
options: CreateOptionsDto[];
|
||||
|
||||
@IsUUID('4')
|
||||
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;
|
||||
|
|
@ -39,4 +40,4 @@ export class CreateFormResultDto extends BaseDto {
|
|||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateOpinionDto)
|
||||
opinions: CreateOpinionDto[];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
import { Entity, Column, ManyToOne, OneToOne } from 'typeorm';
|
||||
import { Entity, Column } from 'typeorm';
|
||||
import { BaseEntity } from '../../_core/entity/_base.entity';
|
||||
import { Realm } from '../../realm/entity/realm.entity';
|
||||
import { Form } from '../../form/entity/form.entity';
|
||||
|
||||
// Options class (embedded)
|
||||
export class Options {
|
||||
|
|
@ -15,11 +13,10 @@ export class Options {
|
|||
// Opinion class (embedded)
|
||||
export class Opinion {
|
||||
@Column(() => Options)
|
||||
options: Options;
|
||||
options: Options[];
|
||||
|
||||
@Column({
|
||||
type: 'uuid',
|
||||
unique: true,
|
||||
})
|
||||
questionId: string;
|
||||
}
|
||||
|
|
@ -43,4 +40,4 @@ export class FormResult extends BaseEntity {
|
|||
|
||||
@Column(() => Opinion)
|
||||
opinions: Opinion[];
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ export class FormResultController {
|
|||
@Get(':id/refresh')
|
||||
async getFormStatistics( // updates data in database and then returns it
|
||||
@Param('formId', new ParseUUIDPipe()) formId: string,
|
||||
): Promise<FormResult> {
|
||||
): Promise<FormResult | null> {
|
||||
return this.formResultService.getFormStatistics(formId);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,5 +13,6 @@ import { Answer } from '../answer/entity/answer.entity';
|
|||
],
|
||||
controllers: [FormResultController],
|
||||
providers: [FormResultService],
|
||||
exports: [FormResultService],
|
||||
})
|
||||
export class FormResultModule {}
|
||||
|
|
|
|||
|
|
@ -3,40 +3,33 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||
import { FormResult } from './entity/formResult.entity';
|
||||
import { CreateFormResultDto } from './dto/create-formResult.dto';
|
||||
import { UpdateFormResultDto } from './dto/update-formResult.dto';
|
||||
import { Answer } from 'src/answer/entity/answer.entity';
|
||||
import { Question } from '../question/entity/question.entity';
|
||||
import { Form } from '../form/entity/form.entity';
|
||||
import { FormSection } from '../formSection/entity/formSection.entity';
|
||||
import { FormPage } from '../formPage/entity/formPage.entity';
|
||||
import { Repository, In } from 'typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Queue } from 'bullmq';
|
||||
|
||||
@Injectable()
|
||||
export class FormResultService {
|
||||
private readonly statisticsQueue: Queue;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Form)
|
||||
private readonly formRepository: Repository<Form>,
|
||||
@InjectRepository(FormResult)
|
||||
private readonly formResultRepo: Repository<FormResult>,
|
||||
@InjectRepository(Question)
|
||||
private readonly questionRepository: Repository<Question>,
|
||||
@InjectRepository(Answer)
|
||||
private readonly answerRepository: Repository<Answer>,
|
||||
@InjectRepository(Form)
|
||||
@InjectRepository(FormPage)
|
||||
private readonly formPageRepository: Repository<FormPage>,
|
||||
@InjectRepository(FormSection)
|
||||
private readonly formSectionRepository: Repository<FormSection>,
|
||||
@InjectRepository(Question)
|
||||
@InjectRepository(Answer)
|
||||
@InjectRepository(FormResult)
|
||||
private readonly formResultRepository: Repository<FormResult>,
|
||||
) {}
|
||||
) {
|
||||
this.statisticsQueue = new Queue('form-statistics', {
|
||||
connection: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: CreateFormResultDto): Promise<FormResult> {
|
||||
const formResult = this.formResultRepo.create({
|
||||
...data,
|
||||
});
|
||||
return await this.formResultRepo.save(formResult);
|
||||
const savedFormResult = await this.formResultRepo.save(formResult);
|
||||
// Schedule statistics update every 10 minutes
|
||||
await this.scheduleFormStatistics(savedFormResult.formId);
|
||||
return savedFormResult;
|
||||
}
|
||||
|
||||
async findAll(
|
||||
|
|
@ -56,7 +49,6 @@ export class FormResultService {
|
|||
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);
|
||||
|
|
@ -76,7 +68,6 @@ export class FormResultService {
|
|||
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
|
||||
});
|
||||
if (!formResult) {
|
||||
throw new NotFoundException(`FormResult with ID "${id}" not found`);
|
||||
|
|
@ -106,72 +97,24 @@ export class FormResultService {
|
|||
}
|
||||
}
|
||||
|
||||
async getFormStatistics(formId: string): Promise<FormResult> {
|
||||
// 1. Find the Form entity
|
||||
const form = await this.formRepository.findOne({ where: { id: formId } });
|
||||
if (!form) {
|
||||
throw new NotFoundException(`Form with ID "${formId}" not found`);
|
||||
}
|
||||
|
||||
// 2. Compute numQuestions
|
||||
const pageIds = form.pageIds || [];
|
||||
const formPages = await this.formPageRepository.find({ where: { id: In(pageIds) } });
|
||||
const formSectionIds = formPages.flatMap(page => page.formSectionIds || []);
|
||||
const formSections = await this.formSectionRepository.find({ where: { id: In(formSectionIds) } });
|
||||
const questionIds = formSections.flatMap(section => section.questionIds || []);
|
||||
const numQuestions = questionIds.length;
|
||||
|
||||
// 3. Compute numParticipants (distinct participantId from answers)
|
||||
const numParticipantsResult = await this.answerRepository
|
||||
.createQueryBuilder('answer')
|
||||
.select('COUNT(DISTINCT answer.participantId)', 'count')
|
||||
.where('answer.questionId IN (:...questionIds)', { questionIds: questionIds.length ? questionIds : ['none'] }) // Handle empty questionIds
|
||||
.getRawOne();
|
||||
const numParticipants = numParticipantsResult ? Number(numParticipantsResult.count) : 0;
|
||||
|
||||
// 4. Compute numAnswers
|
||||
const numAnswers = await this.answerRepository.count({
|
||||
where: { questionId: In(questionIds.length ? questionIds : ['none']) }, // Handle empty questionIds
|
||||
});
|
||||
|
||||
// 5. Compute numCompleteParticipants (participants who answered all questions)
|
||||
const subQuery = this.answerRepository
|
||||
.createQueryBuilder('answer')
|
||||
.select('answer.participantId')
|
||||
.where('answer.questionId IN (:...questionIds)', { questionIds: questionIds.length ? questionIds : ['none'] }) // Handle empty questionIds
|
||||
.groupBy('answer.participantId')
|
||||
.having('COUNT(DISTINCT answer.questionId) = :numQuestions', { numQuestions: numQuestions || 1 }); // Avoid division by zero
|
||||
|
||||
const numCompleteParticipantsResult = await this.answerRepository.manager
|
||||
.createQueryBuilder()
|
||||
.select('COUNT(*)', 'count')
|
||||
.from(`(${subQuery.getQuery()})`, 'subquery')
|
||||
.setParameters(subQuery.getParameters())
|
||||
.getRawOne();
|
||||
const numCompleteParticipants = numCompleteParticipantsResult ? Number(numCompleteParticipantsResult.count) : 0;
|
||||
|
||||
// 6. Find or create FormResult for the Form
|
||||
let formResult = await this.formResultRepository.findOne({ where: { formId } });
|
||||
async getFormStatistics(formId: string): Promise<FormResult | null> {
|
||||
const formResult = await this.formResultRepo.findOne({ where: { formId } });
|
||||
if (!formResult) {
|
||||
formResult = this.formResultRepository.create({
|
||||
formId, // Link to Form via formId
|
||||
numQuestions,
|
||||
numParticipants,
|
||||
numAnswers,
|
||||
numComplete: numCompleteParticipants,
|
||||
opinions: [], // Initialize as empty or compute if needed
|
||||
status: 'active',
|
||||
});
|
||||
} else {
|
||||
formResult.numQuestions = numQuestions;
|
||||
formResult.numParticipants = numParticipants;
|
||||
formResult.numAnswers = numAnswers;
|
||||
formResult.numComplete = numCompleteParticipants;
|
||||
// Preserve existing opinions or update if needed
|
||||
throw new NotFoundException(`FormResult with formId "${formId}" not found`);
|
||||
}
|
||||
|
||||
// 7. Save and return the FormResult
|
||||
return await this.formResultRepository.save(formResult);
|
||||
return formResult;
|
||||
}
|
||||
|
||||
}
|
||||
async scheduleFormStatistics(formId: string): Promise<void> {
|
||||
await this.statisticsQueue.upsertJobScheduler(
|
||||
`form-statistics-${formId}`,
|
||||
{
|
||||
every: 10 * 60 * 1000, // 10 minutes in milliseconds
|
||||
},
|
||||
{
|
||||
name: 'getFormStatistics',
|
||||
data: { formId },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
import { Worker } from 'bullmq';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, In } from 'typeorm';
|
||||
import { FormResult } from './entity/formResult.entity';
|
||||
import { Form } from '../form/entity/form.entity';
|
||||
import { FormPage } from '../formPage/entity/formPage.entity';
|
||||
import { FormSection } from '../formSection/entity/formSection.entity';
|
||||
import { Question } from '../question/entity/question.entity';
|
||||
import { Answer } from '../answer/entity/answer.entity';
|
||||
|
||||
export class FormResultWorker {
|
||||
constructor(
|
||||
@InjectRepository(Form)
|
||||
private readonly formRepository: Repository<Form>,
|
||||
@InjectRepository(FormResult)
|
||||
private readonly formResultRepo: Repository<FormResult>,
|
||||
@InjectRepository(Question)
|
||||
private readonly questionRepository: Repository<Question>,
|
||||
@InjectRepository(Answer)
|
||||
private readonly answerRepository: Repository<Answer>,
|
||||
@InjectRepository(FormPage)
|
||||
private readonly formPageRepository: Repository<FormPage>,
|
||||
@InjectRepository(FormSection)
|
||||
private readonly formSectionRepository: Repository<FormSection>,
|
||||
) {
|
||||
const worker = new Worker(
|
||||
'form-statistics',
|
||||
async (job) => {
|
||||
const { formId } = job.data;
|
||||
await this.processFormStatistics(formId);
|
||||
console.log(`Processed form statistics for formId: ${formId}`);
|
||||
},
|
||||
{
|
||||
connection: {
|
||||
host: 'localhost',
|
||||
port: 6379,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
console.log(`Job ${job.id} completed`);
|
||||
});
|
||||
|
||||
worker.on('failed', (job, err) => {
|
||||
if (job) {
|
||||
console.error(`Job ${job.id} failed: ${err.message}`);
|
||||
} else {
|
||||
console.error(`Job failed: ${err.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async processFormStatistics(formId: string): Promise<void> {
|
||||
// 1. Find the Form entity
|
||||
const form = await this.formRepository.findOne({ where: { id: formId } });
|
||||
if (!form) {
|
||||
throw new Error(`Form with ID "${formId}" not found`);
|
||||
}
|
||||
|
||||
// 2. Compute numQuestions and questionIds
|
||||
const pageIds = form.pageIds || [];
|
||||
const formPages = await this.formPageRepository.find({ where: { id: In(pageIds) } });
|
||||
const formSectionIds = formPages.flatMap(page => page.formSectionIds || []);
|
||||
const formSections = await this.formSectionRepository.find({ where: { id: In(formSectionIds) } });
|
||||
const questionIds = formSections.flatMap(section => section.questionIds || []);
|
||||
const numQuestions = questionIds.length;
|
||||
|
||||
// 3. Compute numParticipants
|
||||
const numParticipantsResult = await this.answerRepository
|
||||
.createQueryBuilder('answer')
|
||||
.select('COUNT(DISTINCT answer.participantId)', 'count')
|
||||
.where('answer.questionId IN (:...questionIds)', { questionIds: questionIds.length ? questionIds : ['none'] })
|
||||
.getRawOne();
|
||||
const numParticipants = numParticipantsResult ? Number(numParticipantsResult.count) : 0;
|
||||
|
||||
// 4. Compute numAnswers
|
||||
const numAnswers = await this.answerRepository.count({
|
||||
where: { questionId: In(questionIds.length ? questionIds : ['none']) },
|
||||
});
|
||||
|
||||
// 5. Compute numComplete
|
||||
const subQuery = this.answerRepository
|
||||
.createQueryBuilder('answer')
|
||||
.select('answer.participantId')
|
||||
.where('answer.questionId IN (:...questionIds)', { questionIds: questionIds.length ? questionIds : ['none'] })
|
||||
.groupBy('answer.participantId')
|
||||
.having('COUNT(DISTINCT answer.questionId) = :numQuestions', { numQuestions: numQuestions || 1 });
|
||||
const numCompleteParticipantsResult = await this.answerRepository.manager
|
||||
.createQueryBuilder()
|
||||
.select('COUNT(*)', 'count')
|
||||
.from(`(${subQuery.getQuery()})`, 'subquery')
|
||||
.setParameters(subQuery.getParameters())
|
||||
.getRawOne();
|
||||
const numComplete = numCompleteParticipantsResult ? Number(numCompleteParticipantsResult.count) : 0;
|
||||
|
||||
// 6. Compute opinions with question options and their vote counts
|
||||
const questions = await this.questionRepository.find({
|
||||
where: { id: In(questionIds.length ? questionIds : ['none']) },
|
||||
relations: ['options'],
|
||||
});
|
||||
|
||||
const opinions = await Promise.all(
|
||||
questions.map(async (question) => {
|
||||
const optionIds = question.optionIds || [];
|
||||
const options = await this.answerRepository
|
||||
.createQueryBuilder('answer')
|
||||
.select('answer.value', 'value')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('answer.questionId = :questionId', { questionId: question.id })
|
||||
.andWhere('answer.value IN (:...optionIds)', { optionIds: optionIds.length ? optionIds : ['none'] })
|
||||
.groupBy('answer.value')
|
||||
.getRawMany();
|
||||
|
||||
const questionOptions = optionIds.map((optionId) => {
|
||||
const optionAnswer = options.find((opt) => opt.value === optionId);
|
||||
return {
|
||||
value: optionId,
|
||||
count: optionAnswer ? Number(optionAnswer.count) : 0,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
questionId: question.id,
|
||||
options: questionOptions,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
// 7. Find or create FormResult
|
||||
let formResult = await this.formResultRepo.findOne({ where: { formId } });
|
||||
if (!formResult) {
|
||||
formResult = this.formResultRepo.create({
|
||||
formId,
|
||||
numQuestions,
|
||||
numParticipants,
|
||||
numAnswers,
|
||||
numComplete,
|
||||
opinions,
|
||||
});
|
||||
} else {
|
||||
formResult.numQuestions = numQuestions;
|
||||
formResult.numParticipants = numParticipants;
|
||||
formResult.numAnswers = numAnswers;
|
||||
formResult.numComplete = numComplete;
|
||||
formResult.opinions = opinions;
|
||||
}
|
||||
|
||||
// 8. Save
|
||||
await this.formResultRepo.save(formResult);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FormResultModule } from './formResult.module';
|
||||
import { FormResultWorker } from './formResult.worker';
|
||||
import { FormResult } from './entity/formResult.entity';
|
||||
import { Form } from '../form/entity/form.entity';
|
||||
import { Question } from '../question/entity/question.entity';
|
||||
import { Answer } from '../answer/entity/answer.entity';
|
||||
import { FormPage } from '../formPage/entity/formPage.entity';
|
||||
import { FormSection } from '../formSection/entity/formSection.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
FormResultModule,
|
||||
TypeOrmModule.forFeature([FormResult, Form, Question, Answer, FormPage, FormSection]),
|
||||
],
|
||||
providers: [FormResultWorker],
|
||||
})
|
||||
export class FormResultWorkerModule {}
|
||||
Loading…
Reference in New Issue