diff --git a/.pnp.cjs b/.pnp.cjs index 355b70d0..c731ce09 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -1677,6 +1677,19 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@confluentinc/kafka-javascript", [\ + ["npm:1.2.0", {\ + "packageLocation": "./.yarn/unplugged/@confluentinc-kafka-javascript-npm-1.2.0-637fe4bdee/node_modules/@confluentinc/kafka-javascript/",\ + "packageDependencies": [\ + ["@confluentinc/kafka-javascript", "npm:1.2.0"],\ + ["@mapbox/node-pre-gyp", "npm:1.0.11"],\ + ["bindings", "npm:1.5.0"],\ + ["nan", "npm:2.22.0"],\ + ["node-gyp", "npm:9.3.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@cspotcode/source-map-support", [\ ["npm:0.8.1", {\ "packageLocation": "./.yarn/cache/@cspotcode-source-map-support-npm-0.8.1-964f2de99d-b6e38a1712.zip/node_modules/@cspotcode/source-map-support/",\ @@ -3172,6 +3185,24 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@mapbox/node-pre-gyp", [\ + ["npm:1.0.11", {\ + "packageLocation": "./.yarn/cache/@mapbox-node-pre-gyp-npm-1.0.11-5547f15a2b-59529a2444.zip/node_modules/@mapbox/node-pre-gyp/",\ + "packageDependencies": [\ + ["@mapbox/node-pre-gyp", "npm:1.0.11"],\ + ["detect-libc", "npm:2.0.3"],\ + ["https-proxy-agent", "npm:5.0.1"],\ + ["make-dir", "npm:3.1.0"],\ + ["node-fetch", "virtual:5547f15a2bb3d361d141532d43f94523f31e9edfe533f8367b3e26e300194e2978be03f56c09e100afcfee4c02b7fbe13c6ffcf58c613b457a86da522a2979f2#npm:2.7.0"],\ + ["nopt", "npm:5.0.0"],\ + ["npmlog", "npm:5.0.1"],\ + ["rimraf", "npm:3.0.2"],\ + ["semver", "npm:7.5.4"],\ + ["tar", "npm:6.1.15"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@mongodb-js/saslprep", [\ ["npm:1.1.0", {\ "packageLocation": "./.yarn/cache/@mongodb-js-saslprep-npm-1.1.0-3906c025b8-1a631b92d2.zip/node_modules/@mongodb-js/saslprep/",\ @@ -3180,6 +3211,14 @@ const RAW_RUNTIME_STATE = ["sparse-bitfield", "npm:3.0.3"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:1.2.0", {\ + "packageLocation": "./.yarn/cache/@mongodb-js-saslprep-npm-1.2.0-d17f8e2605-2c50080844.zip/node_modules/@mongodb-js/saslprep/",\ + "packageDependencies": [\ + ["@mongodb-js/saslprep", "npm:1.2.0"],\ + ["sparse-bitfield", "npm:3.0.3"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@nestjs/cli", [\ @@ -3310,7 +3349,7 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["@nestjs/core", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:10.3.0"],\ ["@nestjs/common", "virtual:ef911e99e751f6eb15acb82bb02e437240c4a171d17ac7123cfdd33420f6779e4226696aad8376f6906f32de2565d849a5a1e38caa759182f2f475712f73607d#npm:10.3.0"],\ - ["@nestjs/microservices", null],\ + ["@nestjs/microservices", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:11.0.7"],\ ["@nestjs/platform-express", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:10.3.0"],\ ["@nestjs/websockets", null],\ ["@nuxtjs/opencollective", "npm:0.3.2"],\ @@ -3462,6 +3501,78 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@nestjs/microservices", [\ + ["npm:11.0.7", {\ + "packageLocation": "./.yarn/cache/@nestjs-microservices-npm-11.0.7-f64a7a6a63-a7147f2ae0.zip/node_modules/@nestjs/microservices/",\ + "packageDependencies": [\ + ["@nestjs/microservices", "npm:11.0.7"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:11.0.7", {\ + "packageLocation": "./.yarn/__virtual__/@nestjs-microservices-virtual-fb5b446db2/0/cache/@nestjs-microservices-npm-11.0.7-f64a7a6a63-a7147f2ae0.zip/node_modules/@nestjs/microservices/",\ + "packageDependencies": [\ + ["@nestjs/microservices", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:11.0.7"],\ + ["@grpc/grpc-js", null],\ + ["@nestjs/common", "virtual:ef911e99e751f6eb15acb82bb02e437240c4a171d17ac7123cfdd33420f6779e4226696aad8376f6906f32de2565d849a5a1e38caa759182f2f475712f73607d#npm:10.3.0"],\ + ["@nestjs/core", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:10.3.0"],\ + ["@nestjs/websockets", null],\ + ["@types/amqp-connection-manager", null],\ + ["@types/amqplib", null],\ + ["@types/cache-manager", null],\ + ["@types/grpc__grpc-js", null],\ + ["@types/ioredis", null],\ + ["@types/kafkajs", null],\ + ["@types/mqtt", null],\ + ["@types/nats", null],\ + ["@types/nestjs__common", null],\ + ["@types/nestjs__core", null],\ + ["@types/nestjs__websockets", null],\ + ["@types/reflect-metadata", null],\ + ["@types/rxjs", null],\ + ["amqp-connection-manager", null],\ + ["amqplib", null],\ + ["cache-manager", null],\ + ["ioredis", null],\ + ["iterare", "npm:1.2.1"],\ + ["kafkajs", "npm:2.2.4"],\ + ["mqtt", null],\ + ["nats", null],\ + ["reflect-metadata", "npm:0.1.13"],\ + ["rxjs", "npm:7.8.1"],\ + ["tslib", "npm:2.8.1"]\ + ],\ + "packagePeers": [\ + "@grpc/grpc-js",\ + "@nestjs/common",\ + "@nestjs/core",\ + "@nestjs/websockets",\ + "@types/amqp-connection-manager",\ + "@types/amqplib",\ + "@types/cache-manager",\ + "@types/grpc__grpc-js",\ + "@types/ioredis",\ + "@types/kafkajs",\ + "@types/mqtt",\ + "@types/nats",\ + "@types/nestjs__common",\ + "@types/nestjs__core",\ + "@types/nestjs__websockets",\ + "@types/reflect-metadata",\ + "@types/rxjs",\ + "amqp-connection-manager",\ + "amqplib",\ + "cache-manager",\ + "ioredis",\ + "kafkajs",\ + "mqtt",\ + "nats",\ + "reflect-metadata",\ + "rxjs"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@nestjs/mongoose", [\ ["npm:10.0.2", {\ "packageLocation": "./.yarn/cache/@nestjs-mongoose-npm-10.0.2-2d4bd943c4-803440ff88.zip/node_modules/@nestjs/mongoose/",\ @@ -3716,6 +3827,51 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@nestjs/schedule", [\ + ["npm:5.0.1", {\ + "packageLocation": "./.yarn/cache/@nestjs-schedule-npm-5.0.1-4c0c74a13b-fe14ce3bd0.zip/node_modules/@nestjs/schedule/",\ + "packageDependencies": [\ + ["@nestjs/schedule", "npm:5.0.1"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:5.0.1", {\ + "packageLocation": "./.yarn/__virtual__/@nestjs-schedule-virtual-53bd19101e/0/cache/@nestjs-schedule-npm-5.0.1-4c0c74a13b-fe14ce3bd0.zip/node_modules/@nestjs/schedule/",\ + "packageDependencies": [\ + ["@nestjs/schedule", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:5.0.1"],\ + ["@nestjs/common", "virtual:ef911e99e751f6eb15acb82bb02e437240c4a171d17ac7123cfdd33420f6779e4226696aad8376f6906f32de2565d849a5a1e38caa759182f2f475712f73607d#npm:10.3.0"],\ + ["@nestjs/core", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:10.3.0"],\ + ["@types/nestjs__common", null],\ + ["@types/nestjs__core", null],\ + ["cron", "npm:3.5.0"]\ + ],\ + "packagePeers": [\ + "@nestjs/common",\ + "@nestjs/core",\ + "@types/nestjs__common",\ + "@types/nestjs__core"\ + ],\ + "linkType": "HARD"\ + }],\ + ["virtual:cfdbbaf53af87e056d7cde7bc92962e5fc54b10c8ebda6abfda1066b1044a3c5391849a370a5a0589066f908fcb5d109ab574d891f2828f6e903a3c099c3f1fc#npm:5.0.1", {\ + "packageLocation": "./.yarn/__virtual__/@nestjs-schedule-virtual-72ba541543/0/cache/@nestjs-schedule-npm-5.0.1-4c0c74a13b-fe14ce3bd0.zip/node_modules/@nestjs/schedule/",\ + "packageDependencies": [\ + ["@nestjs/schedule", "virtual:cfdbbaf53af87e056d7cde7bc92962e5fc54b10c8ebda6abfda1066b1044a3c5391849a370a5a0589066f908fcb5d109ab574d891f2828f6e903a3c099c3f1fc#npm:5.0.1"],\ + ["@nestjs/common", "virtual:ef911e99e751f6eb15acb82bb02e437240c4a171d17ac7123cfdd33420f6779e4226696aad8376f6906f32de2565d849a5a1e38caa759182f2f475712f73607d#npm:10.3.0"],\ + ["@nestjs/core", "virtual:cfdbbaf53af87e056d7cde7bc92962e5fc54b10c8ebda6abfda1066b1044a3c5391849a370a5a0589066f908fcb5d109ab574d891f2828f6e903a3c099c3f1fc#npm:10.3.0"],\ + ["@types/nestjs__common", null],\ + ["@types/nestjs__core", null],\ + ["cron", "npm:3.5.0"]\ + ],\ + "packagePeers": [\ + "@nestjs/common",\ + "@nestjs/core",\ + "@types/nestjs__common",\ + "@types/nestjs__core"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@nestjs/schematics", [\ ["npm:10.0.2", {\ "packageLocation": "./.yarn/cache/@nestjs-schematics-npm-10.0.2-2d958564c6-9e3d71b0c9.zip/node_modules/@nestjs/schematics/",\ @@ -3757,7 +3913,7 @@ const RAW_RUNTIME_STATE = ["@nestjs/testing", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:10.3.0"],\ ["@nestjs/common", "virtual:ef911e99e751f6eb15acb82bb02e437240c4a171d17ac7123cfdd33420f6779e4226696aad8376f6906f32de2565d849a5a1e38caa759182f2f475712f73607d#npm:10.3.0"],\ ["@nestjs/core", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:10.3.0"],\ - ["@nestjs/microservices", null],\ + ["@nestjs/microservices", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:11.0.7"],\ ["@nestjs/platform-express", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:10.3.0"],\ ["@types/nestjs__common", null],\ ["@types/nestjs__core", null],\ @@ -5434,6 +5590,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@types/luxon", [\ + ["npm:3.4.2", {\ + "packageLocation": "./.yarn/cache/@types-luxon-npm-3.4.2-092583f006-fd89566e30.zip/node_modules/@types/luxon/",\ + "packageDependencies": [\ + ["@types/luxon", "npm:3.4.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@types/mime", [\ ["npm:1.3.2", {\ "packageLocation": "./.yarn/cache/@types-mime-npm-1.3.2-ea71878ab3-0493368244.zip/node_modules/@types/mime/",\ @@ -6789,6 +6954,13 @@ const RAW_RUNTIME_STATE = ["debug", "virtual:2f4f5d4be0c4271f81a756602d102e02c292a5b30d8b7f5dd19d5368d75df8bb2f1bf516e3f835c2b5501d1663eb437d4f00d6d057bcaee770b46e87267a0fa3#npm:4.3.4"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.1.3", {\ + "packageLocation": "./.yarn/cache/agent-base-npm-7.1.3-b2c16e72fb-3db6d8d465.zip/node_modules/agent-base/",\ + "packageDependencies": [\ + ["agent-base", "npm:7.1.3"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["agentkeepalive", [\ @@ -7053,6 +7225,15 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["are-we-there-yet", [\ + ["npm:2.0.0", {\ + "packageLocation": "./.yarn/cache/are-we-there-yet-npm-2.0.0-7d2f5201ce-ea6f47d14f.zip/node_modules/are-we-there-yet/",\ + "packageDependencies": [\ + ["are-we-there-yet", "npm:2.0.0"],\ + ["delegates", "npm:1.0.0"],\ + ["readable-stream", "npm:3.6.2"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:3.0.1", {\ "packageLocation": "./.yarn/cache/are-we-there-yet-npm-3.0.1-3395b1512f-390731720e.zip/node_modules/are-we-there-yet/",\ "packageDependencies": [\ @@ -7218,6 +7399,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["async-mutex", [\ + ["npm:0.5.0", {\ + "packageLocation": "./.yarn/cache/async-mutex-npm-0.5.0-cc288ce63d-4c6bfce1cc.zip/node_modules/async-mutex/",\ + "packageDependencies": [\ + ["async-mutex", "npm:0.5.0"],\ + ["tslib", "npm:2.8.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["async-validator", [\ ["npm:4.2.5", {\ "packageLocation": "./.yarn/cache/async-validator-npm-4.2.5-4d61110c66-d77e43bb63.zip/node_modules/async-validator/",\ @@ -7296,6 +7487,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["b4a", [\ + ["npm:1.6.7", {\ + "packageLocation": "./.yarn/cache/b4a-npm-1.6.7-a52d28b4e2-1ac056e3bc.zip/node_modules/b4a/",\ + "packageDependencies": [\ + ["b4a", "npm:1.6.7"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["babel-jest", [\ ["npm:29.5.0", {\ "packageLocation": "./.yarn/cache/babel-jest-npm-29.5.0-7423e21d96-33bad2c4e5.zip/node_modules/babel-jest/",\ @@ -7478,6 +7678,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["bare-events", [\ + ["npm:2.5.4", {\ + "packageLocation": "./.yarn/cache/bare-events-npm-2.5.4-ff815c1e18-135ef380b1.zip/node_modules/bare-events/",\ + "packageDependencies": [\ + ["bare-events", "npm:2.5.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["base64-arraybuffer", [\ ["npm:1.0.2", {\ "packageLocation": "./.yarn/cache/base64-arraybuffer-npm-1.0.2-18f4899892-15e6400d2d.zip/node_modules/base64-arraybuffer/",\ @@ -7523,6 +7732,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["bindings", [\ + ["npm:1.5.0", {\ + "packageLocation": "./.yarn/cache/bindings-npm-1.5.0-77ce1d213c-593d5ae975.zip/node_modules/bindings/",\ + "packageDependencies": [\ + ["bindings", "npm:1.5.0"],\ + ["file-uri-to-path", "npm:1.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["bl", [\ ["npm:4.1.0", {\ "packageLocation": "./.yarn/cache/bl-npm-4.1.0-7f94cdcf3f-b7904e66ed.zip/node_modules/bl/",\ @@ -7667,6 +7886,13 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["bson", [\ + ["npm:6.10.3", {\ + "packageLocation": "./.yarn/cache/bson-npm-6.10.3-f15e20e8f4-53693ad763.zip/node_modules/bson/",\ + "packageDependencies": [\ + ["bson", "npm:6.10.3"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:6.2.0", {\ "packageLocation": "./.yarn/cache/bson-npm-6.2.0-3d364484ee-6e079d1caf.zip/node_modules/bson/",\ "packageDependencies": [\ @@ -7696,6 +7922,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["buffer-crc32", [\ + ["npm:0.2.13", {\ + "packageLocation": "./.yarn/cache/buffer-crc32-npm-0.2.13-c4b6fceac1-06252347ae.zip/node_modules/buffer-crc32/",\ + "packageDependencies": [\ + ["buffer-crc32", "npm:0.2.13"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["buffer-from", [\ ["npm:1.1.2", {\ "packageLocation": "./.yarn/cache/buffer-from-npm-1.1.2-03d2f20d7e-0448524a56.zip/node_modules/buffer-from/",\ @@ -8131,6 +8366,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["commondir", [\ + ["npm:1.0.1", {\ + "packageLocation": "./.yarn/cache/commondir-npm-1.0.1-291b790340-4620bc4936.zip/node_modules/commondir/",\ + "packageDependencies": [\ + ["commondir", "npm:1.0.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["component-emitter", [\ ["npm:1.3.0", {\ "packageLocation": "./.yarn/cache/component-emitter-npm-1.3.0-4b848565b9-dfc1ec2e7a.zip/node_modules/component-emitter/",\ @@ -8371,6 +8615,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["cron", [\ + ["npm:3.5.0", {\ + "packageLocation": "./.yarn/cache/cron-npm-3.5.0-33987e9355-0e667d87c9.zip/node_modules/cron/",\ + "packageDependencies": [\ + ["cron", "npm:3.5.0"],\ + ["@types/luxon", "npm:3.4.2"],\ + ["luxon", "npm:3.5.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["cross-env", [\ ["npm:7.0.3", {\ "packageLocation": "./.yarn/cache/cross-env-npm-7.0.3-96d81820f4-e99911f0d3.zip/node_modules/cross-env/",\ @@ -8499,6 +8754,13 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ + ["npm:4.4.0", {\ + "packageLocation": "./.yarn/cache/debug-npm-4.4.0-f6efe76023-1847944c2e.zip/node_modules/debug/",\ + "packageDependencies": [\ + ["debug", "npm:4.4.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ ["virtual:2f4f5d4be0c4271f81a756602d102e02c292a5b30d8b7f5dd19d5368d75df8bb2f1bf516e3f835c2b5501d1663eb437d4f00d6d057bcaee770b46e87267a0fa3#npm:4.3.4", {\ "packageLocation": "./.yarn/__virtual__/debug-virtual-cb0d589b8b/0/cache/debug-npm-4.3.4-4513954577-0073c3bcbd.zip/node_modules/debug/",\ "packageDependencies": [\ @@ -8540,6 +8802,20 @@ const RAW_RUNTIME_STATE = "supports-color"\ ],\ "linkType": "HARD"\ + }],\ + ["virtual:85bdf3f8a071bff51b00e05876e35473fd4a09308896030c6ee40efe03a8d1d5bbfe1a0232ee72d8b535927c13f24c92b1eadd94038f4f7b2c6e729614d39d65#npm:4.4.0", {\ + "packageLocation": "./.yarn/__virtual__/debug-virtual-4d5b54dc95/0/cache/debug-npm-4.4.0-f6efe76023-1847944c2e.zip/node_modules/debug/",\ + "packageDependencies": [\ + ["debug", "virtual:85bdf3f8a071bff51b00e05876e35473fd4a09308896030c6ee40efe03a8d1d5bbfe1a0232ee72d8b535927c13f24c92b1eadd94038f4f7b2c6e729614d39d65#npm:4.4.0"],\ + ["@types/supports-color", null],\ + ["ms", "npm:2.1.3"],\ + ["supports-color", null]\ + ],\ + "packagePeers": [\ + "@types/supports-color",\ + "supports-color"\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["dedent", [\ @@ -8705,6 +8981,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["detect-libc", [\ + ["npm:2.0.3", {\ + "packageLocation": "./.yarn/cache/detect-libc-npm-2.0.3-2ddae34945-b4ea018d62.zip/node_modules/detect-libc/",\ + "packageDependencies": [\ + ["detect-libc", "npm:2.0.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["detect-newline", [\ ["npm:3.1.0", {\ "packageLocation": "./.yarn/cache/detect-newline-npm-3.1.0-6d33fa8d37-ae6cd429c4.zip/node_modules/detect-newline/",\ @@ -10491,6 +10776,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["fast-fifo", [\ + ["npm:1.3.2", {\ + "packageLocation": "./.yarn/cache/fast-fifo-npm-1.3.2-391cc25df4-6bfcba3e4d.zip/node_modules/fast-fifo/",\ + "packageDependencies": [\ + ["fast-fifo", "npm:1.3.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["fast-glob", [\ ["npm:3.2.12", {\ "packageLocation": "./.yarn/cache/fast-glob-npm-3.2.12-162763bbae-641e748664.zip/node_modules/fast-glob/",\ @@ -10627,6 +10921,7 @@ const RAW_RUNTIME_STATE = ["@nestjs/core", "virtual:cfdbbaf53af87e056d7cde7bc92962e5fc54b10c8ebda6abfda1066b1044a3c5391849a370a5a0589066f908fcb5d109ab574d891f2828f6e903a3c099c3f1fc#npm:10.3.0"],\ ["@nestjs/mongoose", "virtual:cfdbbaf53af87e056d7cde7bc92962e5fc54b10c8ebda6abfda1066b1044a3c5391849a370a5a0589066f908fcb5d109ab574d891f2828f6e903a3c099c3f1fc#npm:10.0.2"],\ ["@nestjs/platform-express", "virtual:cfdbbaf53af87e056d7cde7bc92962e5fc54b10c8ebda6abfda1066b1044a3c5391849a370a5a0589066f908fcb5d109ab574d891f2828f6e903a3c099c3f1fc#npm:10.3.0"],\ + ["@nestjs/schedule", "virtual:cfdbbaf53af87e056d7cde7bc92962e5fc54b10c8ebda6abfda1066b1044a3c5391849a370a5a0589066f908fcb5d109ab574d891f2828f6e903a3c099c3f1fc#npm:5.0.1"],\ ["@nestjs/schematics", "virtual:ef911e99e751f6eb15acb82bb02e437240c4a171d17ac7123cfdd33420f6779e4226696aad8376f6906f32de2565d849a5a1e38caa759182f2f475712f73607d#npm:10.0.2"],\ ["@nestjs/testing", "virtual:cfdbbaf53af87e056d7cde7bc92962e5fc54b10c8ebda6abfda1066b1044a3c5391849a370a5a0589066f908fcb5d109ab574d891f2828f6e903a3c099c3f1fc#npm:10.3.0"],\ ["@toss/date", "npm:1.1.7"],\ @@ -10654,8 +10949,10 @@ const RAW_RUNTIME_STATE = ["eslint-plugin-sort-keys-fix", "npm:1.1.2"],\ ["eslint-plugin-unused-imports", "virtual:ef911e99e751f6eb15acb82bb02e437240c4a171d17ac7123cfdd33420f6779e4226696aad8376f6906f32de2565d849a5a1e38caa759182f2f475712f73607d#npm:2.0.0"],\ ["jest", "virtual:ef911e99e751f6eb15acb82bb02e437240c4a171d17ac7123cfdd33420f6779e4226696aad8376f6906f32de2565d849a5a1e38caa759182f2f475712f73607d#npm:29.5.0"],\ + ["kafkajs", "npm:2.2.4"],\ ["lodash", "npm:4.17.21"],\ ["mongodb", "virtual:39c4fc9ae1549bc48a71ddefde7541201b8fcad41292f3f5350d41410811afb5c87fd6d12cb9821178f88a4260f24b394dd2ba5da21bc7f2ce426d86eae3664a#npm:6.3.0"],\ + ["mongodb-memory-server", "npm:10.1.4"],\ ["mongoose", "npm:8.1.0"],\ ["openai", "virtual:cfdbbaf53af87e056d7cde7bc92962e5fc54b10c8ebda6abfda1066b1044a3c5391849a370a5a0589066f908fcb5d109ab574d891f2828f6e903a3c099c3f1fc#npm:4.85.1"],\ ["prettier", "npm:2.8.8"],\ @@ -10721,6 +11018,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["file-uri-to-path", [\ + ["npm:1.0.0", {\ + "packageLocation": "./.yarn/cache/file-uri-to-path-npm-1.0.0-1043ac6206-b648580bdd.zip/node_modules/file-uri-to-path/",\ + "packageDependencies": [\ + ["file-uri-to-path", "npm:1.0.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["fill-range", [\ ["npm:7.0.1", {\ "packageLocation": "./.yarn/cache/fill-range-npm-7.0.1-b8b1817caa-e260f7592f.zip/node_modules/fill-range/",\ @@ -10755,6 +11061,18 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["find-cache-dir", [\ + ["npm:3.3.2", {\ + "packageLocation": "./.yarn/cache/find-cache-dir-npm-3.3.2-836e68dd83-3907c2e0b1.zip/node_modules/find-cache-dir/",\ + "packageDependencies": [\ + ["find-cache-dir", "npm:3.3.2"],\ + ["commondir", "npm:1.0.1"],\ + ["make-dir", "npm:3.1.0"],\ + ["pkg-dir", "npm:4.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["find-root", [\ ["npm:1.1.0", {\ "packageLocation": "./.yarn/cache/find-root-npm-1.1.0-a16a94005f-caa799c976.zip/node_modules/find-root/",\ @@ -10820,6 +11138,28 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["follow-redirects", [\ + ["npm:1.15.9", {\ + "packageLocation": "./.yarn/cache/follow-redirects-npm-1.15.9-539785d34c-e3ab42d109.zip/node_modules/follow-redirects/",\ + "packageDependencies": [\ + ["follow-redirects", "npm:1.15.9"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:85bdf3f8a071bff51b00e05876e35473fd4a09308896030c6ee40efe03a8d1d5bbfe1a0232ee72d8b535927c13f24c92b1eadd94038f4f7b2c6e729614d39d65#npm:1.15.9", {\ + "packageLocation": "./.yarn/__virtual__/follow-redirects-virtual-42c2cc8245/0/cache/follow-redirects-npm-1.15.9-539785d34c-e3ab42d109.zip/node_modules/follow-redirects/",\ + "packageDependencies": [\ + ["follow-redirects", "virtual:85bdf3f8a071bff51b00e05876e35473fd4a09308896030c6ee40efe03a8d1d5bbfe1a0232ee72d8b535927c13f24c92b1eadd94038f4f7b2c6e729614d39d65#npm:1.15.9"],\ + ["@types/debug", null],\ + ["debug", "virtual:85bdf3f8a071bff51b00e05876e35473fd4a09308896030c6ee40efe03a8d1d5bbfe1a0232ee72d8b535927c13f24c92b1eadd94038f4f7b2c6e729614d39d65#npm:4.4.0"]\ + ],\ + "packagePeers": [\ + "@types/debug",\ + "debug"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["for-each", [\ ["npm:0.3.3", {\ "packageLocation": "./.yarn/cache/for-each-npm-0.3.3-0010ca8cdd-fdac0cde1b.zip/node_modules/for-each/",\ @@ -11032,6 +11372,22 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["gauge", [\ + ["npm:3.0.2", {\ + "packageLocation": "./.yarn/cache/gauge-npm-3.0.2-9e22f7af9e-46df086451.zip/node_modules/gauge/",\ + "packageDependencies": [\ + ["gauge", "npm:3.0.2"],\ + ["aproba", "npm:2.0.0"],\ + ["color-support", "npm:1.1.3"],\ + ["console-control-strings", "npm:1.1.0"],\ + ["has-unicode", "npm:2.0.1"],\ + ["object-assign", "npm:4.1.1"],\ + ["signal-exit", "npm:3.0.7"],\ + ["string-width", "npm:4.2.3"],\ + ["strip-ansi", "npm:6.0.1"],\ + ["wide-align", "npm:1.1.5"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:4.0.4", {\ "packageLocation": "./.yarn/cache/gauge-npm-4.0.4-8f878385e9-09535dd53b.zip/node_modules/gauge/",\ "packageDependencies": [\ @@ -11518,6 +11874,15 @@ const RAW_RUNTIME_STATE = ["debug", "virtual:2f4f5d4be0c4271f81a756602d102e02c292a5b30d8b7f5dd19d5368d75df8bb2f1bf516e3f835c2b5501d1663eb437d4f00d6d057bcaee770b46e87267a0fa3#npm:4.3.4"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.0.6", {\ + "packageLocation": "./.yarn/cache/https-proxy-agent-npm-7.0.6-27a95c2690-784b628cbd.zip/node_modules/https-proxy-agent/",\ + "packageDependencies": [\ + ["https-proxy-agent", "npm:7.0.6"],\ + ["agent-base", "npm:7.1.3"],\ + ["debug", "virtual:2f4f5d4be0c4271f81a756602d102e02c292a5b30d8b7f5dd19d5368d75df8bb2f1bf516e3f835c2b5501d1663eb437d4f00d6d057bcaee770b46e87267a0fa3#npm:4.3.4"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["human-signals", [\ @@ -12995,6 +13360,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["kafkajs", [\ + ["npm:2.2.4", {\ + "packageLocation": "./.yarn/cache/kafkajs-npm-2.2.4-8837bee290-75eb0d2213.zip/node_modules/kafkajs/",\ + "packageDependencies": [\ + ["kafkajs", "npm:2.2.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["kareem", [\ ["npm:2.5.1", {\ "packageLocation": "./.yarn/cache/kareem-npm-2.5.1-79134fb43e-6eb48a3dfb.zip/node_modules/kareem/",\ @@ -13111,12 +13485,15 @@ const RAW_RUNTIME_STATE = "packageDependencies": [\ ["koi-server", "workspace:app/koi-server"],\ ["@codegenie/serverless-express", "npm:4.13.0"],\ + ["@confluentinc/kafka-javascript", "npm:1.2.0"],\ ["@nestjs/cli", "virtual:ce395be6b7426ea598ea4e8ed1669116613a7e400e65e428fdb6306685b33da86346a368fe102e4a42279e9137bdf836fdf4043cbb75e24b471a2ccb7a6a329b#npm:10.3.0"],\ ["@nestjs/common", "virtual:ef911e99e751f6eb15acb82bb02e437240c4a171d17ac7123cfdd33420f6779e4226696aad8376f6906f32de2565d849a5a1e38caa759182f2f475712f73607d#npm:10.3.0"],\ ["@nestjs/config", "virtual:ef911e99e751f6eb15acb82bb02e437240c4a171d17ac7123cfdd33420f6779e4226696aad8376f6906f32de2565d849a5a1e38caa759182f2f475712f73607d#npm:3.1.1"],\ ["@nestjs/core", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:10.3.0"],\ + ["@nestjs/microservices", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:11.0.7"],\ ["@nestjs/mongoose", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:10.0.2"],\ ["@nestjs/platform-express", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:10.3.0"],\ + ["@nestjs/schedule", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:5.0.1"],\ ["@nestjs/schematics", "virtual:ef911e99e751f6eb15acb82bb02e437240c4a171d17ac7123cfdd33420f6779e4226696aad8376f6906f32de2565d849a5a1e38caa759182f2f475712f73607d#npm:10.0.2"],\ ["@nestjs/testing", "virtual:388b94cf46ab0fd1abeedc52857c6b636b2520425311e9ea09ccdd112f8cc7b8d6857d1733810bb85d10742c5e6d13263ad6caaa91dfc75d846f0bcc8621a479#npm:10.3.0"],\ ["@toss/date", "npm:1.1.7"],\ @@ -13149,6 +13526,7 @@ const RAW_RUNTIME_STATE = ["feature-nest-poll", "workspace:package/feature/feature-nest-poll"],\ ["feature-nest-stock", "workspace:package/feature/feature-nest-stock"],\ ["jest", "virtual:ef911e99e751f6eb15acb82bb02e437240c4a171d17ac7123cfdd33420f6779e4226696aad8376f6906f32de2565d849a5a1e38caa759182f2f475712f73607d#npm:29.5.0"],\ + ["kafkajs", "npm:2.2.4"],\ ["mongoose", "npm:8.1.0"],\ ["prettier", "npm:2.8.8"],\ ["reflect-metadata", "npm:0.1.13"],\ @@ -13485,6 +13863,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["luxon", [\ + ["npm:3.5.0", {\ + "packageLocation": "./.yarn/cache/luxon-npm-3.5.0-92bb977f7f-48f86e6c1c.zip/node_modules/luxon/",\ + "packageDependencies": [\ + ["luxon", "npm:3.5.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["magic-string", [\ ["npm:0.30.0", {\ "packageLocation": "./.yarn/cache/magic-string-npm-0.30.0-20d8e0b6e4-81ba9c84f5.zip/node_modules/magic-string/",\ @@ -13886,6 +14273,13 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["mongodb", [\ + ["npm:6.14.2", {\ + "packageLocation": "./.yarn/cache/mongodb-npm-6.14.2-8d237c377c-08f6b42b78.zip/node_modules/mongodb/",\ + "packageDependencies": [\ + ["mongodb", "npm:6.14.2"]\ + ],\ + "linkType": "SOFT"\ + }],\ ["npm:6.3.0", {\ "packageLocation": "./.yarn/cache/mongodb-npm-6.3.0-28854d0f74-3b52734e64.zip/node_modules/mongodb/",\ "packageDependencies": [\ @@ -13932,6 +14326,46 @@ const RAW_RUNTIME_STATE = "socks"\ ],\ "linkType": "HARD"\ + }],\ + ["virtual:85bdf3f8a071bff51b00e05876e35473fd4a09308896030c6ee40efe03a8d1d5bbfe1a0232ee72d8b535927c13f24c92b1eadd94038f4f7b2c6e729614d39d65#npm:6.14.2", {\ + "packageLocation": "./.yarn/__virtual__/mongodb-virtual-0aad0629c4/0/cache/mongodb-npm-6.14.2-8d237c377c-08f6b42b78.zip/node_modules/mongodb/",\ + "packageDependencies": [\ + ["mongodb", "virtual:85bdf3f8a071bff51b00e05876e35473fd4a09308896030c6ee40efe03a8d1d5bbfe1a0232ee72d8b535927c13f24c92b1eadd94038f4f7b2c6e729614d39d65#npm:6.14.2"],\ + ["@aws-sdk/credential-providers", null],\ + ["@mongodb-js/saslprep", "npm:1.2.0"],\ + ["@mongodb-js/zstd", null],\ + ["@types/aws-sdk__credential-providers", null],\ + ["@types/gcp-metadata", null],\ + ["@types/kerberos", null],\ + ["@types/mongodb-client-encryption", null],\ + ["@types/mongodb-js__zstd", null],\ + ["@types/snappy", null],\ + ["@types/socks", null],\ + ["bson", "npm:6.10.3"],\ + ["gcp-metadata", null],\ + ["kerberos", null],\ + ["mongodb-client-encryption", null],\ + ["mongodb-connection-string-url", "npm:3.0.0"],\ + ["snappy", null],\ + ["socks", null]\ + ],\ + "packagePeers": [\ + "@aws-sdk/credential-providers",\ + "@mongodb-js/zstd",\ + "@types/aws-sdk__credential-providers",\ + "@types/gcp-metadata",\ + "@types/kerberos",\ + "@types/mongodb-client-encryption",\ + "@types/mongodb-js__zstd",\ + "@types/snappy",\ + "@types/socks",\ + "gcp-metadata",\ + "kerberos",\ + "mongodb-client-encryption",\ + "snappy",\ + "socks"\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["mongodb-connection-string-url", [\ @@ -13945,6 +14379,38 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["mongodb-memory-server", [\ + ["npm:10.1.4", {\ + "packageLocation": "./.yarn/unplugged/mongodb-memory-server-npm-10.1.4-5c7ce446ca/node_modules/mongodb-memory-server/",\ + "packageDependencies": [\ + ["mongodb-memory-server", "npm:10.1.4"],\ + ["mongodb-memory-server-core", "npm:10.1.4"],\ + ["tslib", "npm:2.8.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["mongodb-memory-server-core", [\ + ["npm:10.1.4", {\ + "packageLocation": "./.yarn/cache/mongodb-memory-server-core-npm-10.1.4-85bdf3f8a0-7ee7fd7af7.zip/node_modules/mongodb-memory-server-core/",\ + "packageDependencies": [\ + ["mongodb-memory-server-core", "npm:10.1.4"],\ + ["async-mutex", "npm:0.5.0"],\ + ["camelcase", "npm:6.3.0"],\ + ["debug", "virtual:85bdf3f8a071bff51b00e05876e35473fd4a09308896030c6ee40efe03a8d1d5bbfe1a0232ee72d8b535927c13f24c92b1eadd94038f4f7b2c6e729614d39d65#npm:4.4.0"],\ + ["find-cache-dir", "npm:3.3.2"],\ + ["follow-redirects", "virtual:85bdf3f8a071bff51b00e05876e35473fd4a09308896030c6ee40efe03a8d1d5bbfe1a0232ee72d8b535927c13f24c92b1eadd94038f4f7b2c6e729614d39d65#npm:1.15.9"],\ + ["https-proxy-agent", "npm:7.0.6"],\ + ["mongodb", "virtual:85bdf3f8a071bff51b00e05876e35473fd4a09308896030c6ee40efe03a8d1d5bbfe1a0232ee72d8b535927c13f24c92b1eadd94038f4f7b2c6e729614d39d65#npm:6.14.2"],\ + ["new-find-package-json", "npm:2.0.0"],\ + ["semver", "npm:7.7.1"],\ + ["tar-stream", "npm:3.1.7"],\ + ["tslib", "npm:2.8.1"],\ + ["yauzl", "npm:3.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["mongoose", [\ ["npm:8.1.0", {\ "packageLocation": "./.yarn/cache/mongoose-npm-8.1.0-39c4fc9ae1-efb79dd816.zip/node_modules/mongoose/",\ @@ -14044,6 +14510,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["nan", [\ + ["npm:2.22.0", {\ + "packageLocation": "./.yarn/unplugged/nan-npm-2.22.0-3750ad85d9/node_modules/nan/",\ + "packageDependencies": [\ + ["nan", "npm:2.22.0"],\ + ["node-gyp", "npm:9.3.1"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["nanoid", [\ ["npm:3.3.6", {\ "packageLocation": "./.yarn/cache/nanoid-npm-3.3.6-e6d6ae7e71-67235c39d1.zip/node_modules/nanoid/",\ @@ -14096,6 +14572,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["new-find-package-json", [\ + ["npm:2.0.0", {\ + "packageLocation": "./.yarn/cache/new-find-package-json-npm-2.0.0-90004ee195-5488ead794.zip/node_modules/new-find-package-json/",\ + "packageDependencies": [\ + ["new-find-package-json", "npm:2.0.0"],\ + ["debug", "virtual:2f4f5d4be0c4271f81a756602d102e02c292a5b30d8b7f5dd19d5368d75df8bb2f1bf516e3f835c2b5501d1663eb437d4f00d6d057bcaee770b46e87267a0fa3#npm:4.3.4"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["node-abort-controller", [\ ["npm:3.1.1", {\ "packageLocation": "./.yarn/cache/node-abort-controller-npm-3.1.1-e246ed42cd-0a2cdb7ec0.zip/node_modules/node-abort-controller/",\ @@ -14139,10 +14625,10 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ - ["virtual:328a05d3af8f67d71ac542595f96731b2a069650a7f7ad95d9eb271726c0d8e1df009b9c2b7bd030c216153323de81a42836946b659984cd35f61d56b94cdffe#npm:2.7.0", {\ - "packageLocation": "./.yarn/__virtual__/node-fetch-virtual-b63af06947/0/cache/node-fetch-npm-2.7.0-587d57004e-b24f8a3dc9.zip/node_modules/node-fetch/",\ + ["virtual:5547f15a2bb3d361d141532d43f94523f31e9edfe533f8367b3e26e300194e2978be03f56c09e100afcfee4c02b7fbe13c6ffcf58c613b457a86da522a2979f2#npm:2.7.0", {\ + "packageLocation": "./.yarn/__virtual__/node-fetch-virtual-64b33d0816/0/cache/node-fetch-npm-2.7.0-587d57004e-b24f8a3dc9.zip/node_modules/node-fetch/",\ "packageDependencies": [\ - ["node-fetch", "virtual:328a05d3af8f67d71ac542595f96731b2a069650a7f7ad95d9eb271726c0d8e1df009b9c2b7bd030c216153323de81a42836946b659984cd35f61d56b94cdffe#npm:2.7.0"],\ + ["node-fetch", "virtual:5547f15a2bb3d361d141532d43f94523f31e9edfe533f8367b3e26e300194e2978be03f56c09e100afcfee4c02b7fbe13c6ffcf58c613b457a86da522a2979f2#npm:2.7.0"],\ ["@types/encoding", null],\ ["encoding", null],\ ["whatwg-url", "npm:5.0.0"]\ @@ -14213,6 +14699,14 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["nopt", [\ + ["npm:5.0.0", {\ + "packageLocation": "./.yarn/cache/nopt-npm-5.0.0-304b40fbfe-00f9bb2d16.zip/node_modules/nopt/",\ + "packageDependencies": [\ + ["nopt", "npm:5.0.0"],\ + ["abbrev", "npm:1.1.1"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:6.0.0", {\ "packageLocation": "./.yarn/cache/nopt-npm-6.0.0-5ea8050815-3c1128e07c.zip/node_modules/nopt/",\ "packageDependencies": [\ @@ -14250,6 +14744,17 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["npmlog", [\ + ["npm:5.0.1", {\ + "packageLocation": "./.yarn/cache/npmlog-npm-5.0.1-366cab64a2-f42c7b9584.zip/node_modules/npmlog/",\ + "packageDependencies": [\ + ["npmlog", "npm:5.0.1"],\ + ["are-we-there-yet", "npm:2.0.0"],\ + ["console-control-strings", "npm:1.1.0"],\ + ["gauge", "npm:3.0.2"],\ + ["set-blocking", "npm:2.0.0"]\ + ],\ + "linkType": "HARD"\ + }],\ ["npm:6.0.2", {\ "packageLocation": "./.yarn/cache/npmlog-npm-6.0.2-e0e69455c7-82b123677e.zip/node_modules/npmlog/",\ "packageDependencies": [\ @@ -14459,7 +14964,7 @@ const RAW_RUNTIME_STATE = ["agentkeepalive", "npm:4.3.0"],\ ["form-data-encoder", "npm:1.7.2"],\ ["formdata-node", "npm:4.4.1"],\ - ["node-fetch", "virtual:328a05d3af8f67d71ac542595f96731b2a069650a7f7ad95d9eb271726c0d8e1df009b9c2b7bd030c216153323de81a42836946b659984cd35f61d56b94cdffe#npm:2.7.0"],\ + ["node-fetch", "virtual:5547f15a2bb3d361d141532d43f94523f31e9edfe533f8367b3e26e300194e2978be03f56c09e100afcfee4c02b7fbe13c6ffcf58c613b457a86da522a2979f2#npm:2.7.0"],\ ["ws", null],\ ["zod", null]\ ],\ @@ -14702,6 +15207,15 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["pend", [\ + ["npm:1.2.0", {\ + "packageLocation": "./.yarn/cache/pend-npm-1.2.0-7a13d93266-6c72f52433.zip/node_modules/pend/",\ + "packageDependencies": [\ + ["pend", "npm:1.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["picocolors", [\ ["npm:1.0.0", {\ "packageLocation": "./.yarn/cache/picocolors-npm-1.0.0-d81e0b1927-a2e8092dd8.zip/node_modules/picocolors/",\ @@ -16837,6 +17351,13 @@ const RAW_RUNTIME_STATE = ["semver", "npm:7.6.3"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:7.7.1", {\ + "packageLocation": "./.yarn/cache/semver-npm-7.7.1-4572475307-4cfa1eb91e.zip/node_modules/semver/",\ + "packageDependencies": [\ + ["semver", "npm:7.7.1"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["send", [\ @@ -17424,6 +17945,18 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["streamx", [\ + ["npm:2.22.0", {\ + "packageLocation": "./.yarn/cache/streamx-npm-2.22.0-6953aefe6d-9c329bb316.zip/node_modules/streamx/",\ + "packageDependencies": [\ + ["streamx", "npm:2.22.0"],\ + ["bare-events", "npm:2.5.4"],\ + ["fast-fifo", "npm:1.3.2"],\ + ["text-decoder", "npm:1.2.3"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["string-convert", [\ ["npm:0.2.1", {\ "packageLocation": "./.yarn/cache/string-convert-npm-0.2.1-f1a42cb9ea-f3eb484a45.zip/node_modules/string-convert/",\ @@ -17731,6 +18264,18 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["tar-stream", [\ + ["npm:3.1.7", {\ + "packageLocation": "./.yarn/cache/tar-stream-npm-3.1.7-c34f9aa00f-b21a82705a.zip/node_modules/tar-stream/",\ + "packageDependencies": [\ + ["tar-stream", "npm:3.1.7"],\ + ["b4a", "npm:1.6.7"],\ + ["fast-fifo", "npm:1.3.2"],\ + ["streamx", "npm:2.22.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["terser", [\ ["npm:5.27.0", {\ "packageLocation": "./.yarn/cache/terser-npm-5.27.0-c18b449e2e-9b2c5cb007.zip/node_modules/terser/",\ @@ -17795,6 +18340,16 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["text-decoder", [\ + ["npm:1.2.3", {\ + "packageLocation": "./.yarn/cache/text-decoder-npm-1.2.3-cc7432569a-bcdec33c0f.zip/node_modules/text-decoder/",\ + "packageDependencies": [\ + ["text-decoder", "npm:1.2.3"],\ + ["b4a", "npm:1.6.7"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["text-segmentation", [\ ["npm:1.0.3", {\ "packageLocation": "./.yarn/cache/text-segmentation-npm-1.0.3-634201c349-86191de83f.zip/node_modules/text-segmentation/",\ @@ -18192,6 +18747,13 @@ const RAW_RUNTIME_STATE = ["tslib", "npm:2.6.2"]\ ],\ "linkType": "HARD"\ + }],\ + ["npm:2.8.1", {\ + "packageLocation": "./.yarn/cache/tslib-npm-2.8.1-66590b21b8-3e2e043d5c.zip/node_modules/tslib/",\ + "packageDependencies": [\ + ["tslib", "npm:2.8.1"]\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["tsutils", [\ @@ -19337,6 +19899,17 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["yauzl", [\ + ["npm:3.2.0", {\ + "packageLocation": "./.yarn/cache/yauzl-npm-3.2.0-22e6762265-a3cd2bfcf7.zip/node_modules/yauzl/",\ + "packageDependencies": [\ + ["yauzl", "npm:3.2.0"],\ + ["buffer-crc32", "npm:0.2.13"],\ + ["pend", "npm:1.2.0"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["yn", [\ ["npm:3.1.1", {\ "packageLocation": "./.yarn/cache/yn-npm-3.1.1-8ad4259784-2c487b0e14.zip/node_modules/yn/",\ diff --git a/.yarn/cache/@confluentinc-kafka-javascript-npm-1.2.0-637fe4bdee-32d1a18e8d.zip b/.yarn/cache/@confluentinc-kafka-javascript-npm-1.2.0-637fe4bdee-32d1a18e8d.zip new file mode 100644 index 00000000..8bc86c29 Binary files /dev/null and b/.yarn/cache/@confluentinc-kafka-javascript-npm-1.2.0-637fe4bdee-32d1a18e8d.zip differ diff --git a/.yarn/cache/@mapbox-node-pre-gyp-npm-1.0.11-5547f15a2b-59529a2444.zip b/.yarn/cache/@mapbox-node-pre-gyp-npm-1.0.11-5547f15a2b-59529a2444.zip new file mode 100644 index 00000000..10a53a14 Binary files /dev/null and b/.yarn/cache/@mapbox-node-pre-gyp-npm-1.0.11-5547f15a2b-59529a2444.zip differ diff --git a/.yarn/cache/@mongodb-js-saslprep-npm-1.2.0-d17f8e2605-2c50080844.zip b/.yarn/cache/@mongodb-js-saslprep-npm-1.2.0-d17f8e2605-2c50080844.zip new file mode 100644 index 00000000..7da2497b Binary files /dev/null and b/.yarn/cache/@mongodb-js-saslprep-npm-1.2.0-d17f8e2605-2c50080844.zip differ diff --git a/.yarn/cache/@nestjs-microservices-npm-11.0.7-f64a7a6a63-a7147f2ae0.zip b/.yarn/cache/@nestjs-microservices-npm-11.0.7-f64a7a6a63-a7147f2ae0.zip new file mode 100644 index 00000000..1411fdd5 Binary files /dev/null and b/.yarn/cache/@nestjs-microservices-npm-11.0.7-f64a7a6a63-a7147f2ae0.zip differ diff --git a/.yarn/cache/@nestjs-schedule-npm-5.0.1-4c0c74a13b-fe14ce3bd0.zip b/.yarn/cache/@nestjs-schedule-npm-5.0.1-4c0c74a13b-fe14ce3bd0.zip new file mode 100644 index 00000000..a9487869 Binary files /dev/null and b/.yarn/cache/@nestjs-schedule-npm-5.0.1-4c0c74a13b-fe14ce3bd0.zip differ diff --git a/.yarn/cache/@types-luxon-npm-3.4.2-092583f006-fd89566e30.zip b/.yarn/cache/@types-luxon-npm-3.4.2-092583f006-fd89566e30.zip new file mode 100644 index 00000000..1ae0bc98 Binary files /dev/null and b/.yarn/cache/@types-luxon-npm-3.4.2-092583f006-fd89566e30.zip differ diff --git a/.yarn/cache/agent-base-npm-7.1.3-b2c16e72fb-3db6d8d465.zip b/.yarn/cache/agent-base-npm-7.1.3-b2c16e72fb-3db6d8d465.zip new file mode 100644 index 00000000..76775d59 Binary files /dev/null and b/.yarn/cache/agent-base-npm-7.1.3-b2c16e72fb-3db6d8d465.zip differ diff --git a/.yarn/cache/are-we-there-yet-npm-2.0.0-7d2f5201ce-ea6f47d14f.zip b/.yarn/cache/are-we-there-yet-npm-2.0.0-7d2f5201ce-ea6f47d14f.zip new file mode 100644 index 00000000..7c02d9d2 Binary files /dev/null and b/.yarn/cache/are-we-there-yet-npm-2.0.0-7d2f5201ce-ea6f47d14f.zip differ diff --git a/.yarn/cache/async-mutex-npm-0.5.0-cc288ce63d-4c6bfce1cc.zip b/.yarn/cache/async-mutex-npm-0.5.0-cc288ce63d-4c6bfce1cc.zip new file mode 100644 index 00000000..d0e17cc1 Binary files /dev/null and b/.yarn/cache/async-mutex-npm-0.5.0-cc288ce63d-4c6bfce1cc.zip differ diff --git a/.yarn/cache/b4a-npm-1.6.7-a52d28b4e2-1ac056e3bc.zip b/.yarn/cache/b4a-npm-1.6.7-a52d28b4e2-1ac056e3bc.zip new file mode 100644 index 00000000..3b86b624 Binary files /dev/null and b/.yarn/cache/b4a-npm-1.6.7-a52d28b4e2-1ac056e3bc.zip differ diff --git a/.yarn/cache/bare-events-npm-2.5.4-ff815c1e18-135ef380b1.zip b/.yarn/cache/bare-events-npm-2.5.4-ff815c1e18-135ef380b1.zip new file mode 100644 index 00000000..b72d34a6 Binary files /dev/null and b/.yarn/cache/bare-events-npm-2.5.4-ff815c1e18-135ef380b1.zip differ diff --git a/.yarn/cache/bindings-npm-1.5.0-77ce1d213c-593d5ae975.zip b/.yarn/cache/bindings-npm-1.5.0-77ce1d213c-593d5ae975.zip new file mode 100644 index 00000000..e3221f74 Binary files /dev/null and b/.yarn/cache/bindings-npm-1.5.0-77ce1d213c-593d5ae975.zip differ diff --git a/.yarn/cache/bson-npm-6.10.3-f15e20e8f4-53693ad763.zip b/.yarn/cache/bson-npm-6.10.3-f15e20e8f4-53693ad763.zip new file mode 100644 index 00000000..35adec31 Binary files /dev/null and b/.yarn/cache/bson-npm-6.10.3-f15e20e8f4-53693ad763.zip differ diff --git a/.yarn/cache/buffer-crc32-npm-0.2.13-c4b6fceac1-06252347ae.zip b/.yarn/cache/buffer-crc32-npm-0.2.13-c4b6fceac1-06252347ae.zip new file mode 100644 index 00000000..96da9d81 Binary files /dev/null and b/.yarn/cache/buffer-crc32-npm-0.2.13-c4b6fceac1-06252347ae.zip differ diff --git a/.yarn/cache/commondir-npm-1.0.1-291b790340-4620bc4936.zip b/.yarn/cache/commondir-npm-1.0.1-291b790340-4620bc4936.zip new file mode 100644 index 00000000..99574e49 Binary files /dev/null and b/.yarn/cache/commondir-npm-1.0.1-291b790340-4620bc4936.zip differ diff --git a/.yarn/cache/cron-npm-3.5.0-33987e9355-0e667d87c9.zip b/.yarn/cache/cron-npm-3.5.0-33987e9355-0e667d87c9.zip new file mode 100644 index 00000000..62164c4a Binary files /dev/null and b/.yarn/cache/cron-npm-3.5.0-33987e9355-0e667d87c9.zip differ diff --git a/.yarn/cache/debug-npm-4.4.0-f6efe76023-1847944c2e.zip b/.yarn/cache/debug-npm-4.4.0-f6efe76023-1847944c2e.zip new file mode 100644 index 00000000..5bce5f62 Binary files /dev/null and b/.yarn/cache/debug-npm-4.4.0-f6efe76023-1847944c2e.zip differ diff --git a/.yarn/cache/detect-libc-npm-2.0.3-2ddae34945-b4ea018d62.zip b/.yarn/cache/detect-libc-npm-2.0.3-2ddae34945-b4ea018d62.zip new file mode 100644 index 00000000..46f4bae9 Binary files /dev/null and b/.yarn/cache/detect-libc-npm-2.0.3-2ddae34945-b4ea018d62.zip differ diff --git a/.yarn/cache/fast-fifo-npm-1.3.2-391cc25df4-6bfcba3e4d.zip b/.yarn/cache/fast-fifo-npm-1.3.2-391cc25df4-6bfcba3e4d.zip new file mode 100644 index 00000000..c99b5983 Binary files /dev/null and b/.yarn/cache/fast-fifo-npm-1.3.2-391cc25df4-6bfcba3e4d.zip differ diff --git a/.yarn/cache/file-uri-to-path-npm-1.0.0-1043ac6206-b648580bdd.zip b/.yarn/cache/file-uri-to-path-npm-1.0.0-1043ac6206-b648580bdd.zip new file mode 100644 index 00000000..afc4b1cf Binary files /dev/null and b/.yarn/cache/file-uri-to-path-npm-1.0.0-1043ac6206-b648580bdd.zip differ diff --git a/.yarn/cache/find-cache-dir-npm-3.3.2-836e68dd83-3907c2e0b1.zip b/.yarn/cache/find-cache-dir-npm-3.3.2-836e68dd83-3907c2e0b1.zip new file mode 100644 index 00000000..84616ad6 Binary files /dev/null and b/.yarn/cache/find-cache-dir-npm-3.3.2-836e68dd83-3907c2e0b1.zip differ diff --git a/.yarn/cache/follow-redirects-npm-1.15.9-539785d34c-e3ab42d109.zip b/.yarn/cache/follow-redirects-npm-1.15.9-539785d34c-e3ab42d109.zip new file mode 100644 index 00000000..b21a4ce7 Binary files /dev/null and b/.yarn/cache/follow-redirects-npm-1.15.9-539785d34c-e3ab42d109.zip differ diff --git a/.yarn/cache/gauge-npm-3.0.2-9e22f7af9e-46df086451.zip b/.yarn/cache/gauge-npm-3.0.2-9e22f7af9e-46df086451.zip new file mode 100644 index 00000000..357aa422 Binary files /dev/null and b/.yarn/cache/gauge-npm-3.0.2-9e22f7af9e-46df086451.zip differ diff --git a/.yarn/cache/https-proxy-agent-npm-7.0.6-27a95c2690-784b628cbd.zip b/.yarn/cache/https-proxy-agent-npm-7.0.6-27a95c2690-784b628cbd.zip new file mode 100644 index 00000000..248a8d21 Binary files /dev/null and b/.yarn/cache/https-proxy-agent-npm-7.0.6-27a95c2690-784b628cbd.zip differ diff --git a/.yarn/cache/kafkajs-npm-2.2.4-8837bee290-75eb0d2213.zip b/.yarn/cache/kafkajs-npm-2.2.4-8837bee290-75eb0d2213.zip new file mode 100644 index 00000000..2a9c8e06 Binary files /dev/null and b/.yarn/cache/kafkajs-npm-2.2.4-8837bee290-75eb0d2213.zip differ diff --git a/.yarn/cache/luxon-npm-3.5.0-92bb977f7f-48f86e6c1c.zip b/.yarn/cache/luxon-npm-3.5.0-92bb977f7f-48f86e6c1c.zip new file mode 100644 index 00000000..e8f33f74 Binary files /dev/null and b/.yarn/cache/luxon-npm-3.5.0-92bb977f7f-48f86e6c1c.zip differ diff --git a/.yarn/cache/mongodb-memory-server-core-npm-10.1.4-85bdf3f8a0-7ee7fd7af7.zip b/.yarn/cache/mongodb-memory-server-core-npm-10.1.4-85bdf3f8a0-7ee7fd7af7.zip new file mode 100644 index 00000000..5391f9cf Binary files /dev/null and b/.yarn/cache/mongodb-memory-server-core-npm-10.1.4-85bdf3f8a0-7ee7fd7af7.zip differ diff --git a/.yarn/cache/mongodb-memory-server-npm-10.1.4-5c7ce446ca-712ae58480.zip b/.yarn/cache/mongodb-memory-server-npm-10.1.4-5c7ce446ca-712ae58480.zip new file mode 100644 index 00000000..17be0aae Binary files /dev/null and b/.yarn/cache/mongodb-memory-server-npm-10.1.4-5c7ce446ca-712ae58480.zip differ diff --git a/.yarn/cache/mongodb-npm-6.14.2-8d237c377c-08f6b42b78.zip b/.yarn/cache/mongodb-npm-6.14.2-8d237c377c-08f6b42b78.zip new file mode 100644 index 00000000..6489e41d Binary files /dev/null and b/.yarn/cache/mongodb-npm-6.14.2-8d237c377c-08f6b42b78.zip differ diff --git a/.yarn/cache/nan-npm-2.22.0-3750ad85d9-ab165ba910.zip b/.yarn/cache/nan-npm-2.22.0-3750ad85d9-ab165ba910.zip new file mode 100644 index 00000000..696658e0 Binary files /dev/null and b/.yarn/cache/nan-npm-2.22.0-3750ad85d9-ab165ba910.zip differ diff --git a/.yarn/cache/new-find-package-json-npm-2.0.0-90004ee195-5488ead794.zip b/.yarn/cache/new-find-package-json-npm-2.0.0-90004ee195-5488ead794.zip new file mode 100644 index 00000000..1bb60abe Binary files /dev/null and b/.yarn/cache/new-find-package-json-npm-2.0.0-90004ee195-5488ead794.zip differ diff --git a/.yarn/cache/nopt-npm-5.0.0-304b40fbfe-00f9bb2d16.zip b/.yarn/cache/nopt-npm-5.0.0-304b40fbfe-00f9bb2d16.zip new file mode 100644 index 00000000..e9176b4a Binary files /dev/null and b/.yarn/cache/nopt-npm-5.0.0-304b40fbfe-00f9bb2d16.zip differ diff --git a/.yarn/cache/npmlog-npm-5.0.1-366cab64a2-f42c7b9584.zip b/.yarn/cache/npmlog-npm-5.0.1-366cab64a2-f42c7b9584.zip new file mode 100644 index 00000000..13d22911 Binary files /dev/null and b/.yarn/cache/npmlog-npm-5.0.1-366cab64a2-f42c7b9584.zip differ diff --git a/.yarn/cache/pend-npm-1.2.0-7a13d93266-6c72f52433.zip b/.yarn/cache/pend-npm-1.2.0-7a13d93266-6c72f52433.zip new file mode 100644 index 00000000..03b6b6de Binary files /dev/null and b/.yarn/cache/pend-npm-1.2.0-7a13d93266-6c72f52433.zip differ diff --git a/.yarn/cache/semver-npm-7.7.1-4572475307-4cfa1eb91e.zip b/.yarn/cache/semver-npm-7.7.1-4572475307-4cfa1eb91e.zip new file mode 100644 index 00000000..5af80126 Binary files /dev/null and b/.yarn/cache/semver-npm-7.7.1-4572475307-4cfa1eb91e.zip differ diff --git a/.yarn/cache/streamx-npm-2.22.0-6953aefe6d-9c329bb316.zip b/.yarn/cache/streamx-npm-2.22.0-6953aefe6d-9c329bb316.zip new file mode 100644 index 00000000..e860bd8b Binary files /dev/null and b/.yarn/cache/streamx-npm-2.22.0-6953aefe6d-9c329bb316.zip differ diff --git a/.yarn/cache/tar-stream-npm-3.1.7-c34f9aa00f-b21a82705a.zip b/.yarn/cache/tar-stream-npm-3.1.7-c34f9aa00f-b21a82705a.zip new file mode 100644 index 00000000..da0b843c Binary files /dev/null and b/.yarn/cache/tar-stream-npm-3.1.7-c34f9aa00f-b21a82705a.zip differ diff --git a/.yarn/cache/text-decoder-npm-1.2.3-cc7432569a-bcdec33c0f.zip b/.yarn/cache/text-decoder-npm-1.2.3-cc7432569a-bcdec33c0f.zip new file mode 100644 index 00000000..26d87235 Binary files /dev/null and b/.yarn/cache/text-decoder-npm-1.2.3-cc7432569a-bcdec33c0f.zip differ diff --git a/.yarn/cache/tslib-npm-2.8.1-66590b21b8-3e2e043d5c.zip b/.yarn/cache/tslib-npm-2.8.1-66590b21b8-3e2e043d5c.zip new file mode 100644 index 00000000..938b8821 Binary files /dev/null and b/.yarn/cache/tslib-npm-2.8.1-66590b21b8-3e2e043d5c.zip differ diff --git a/.yarn/cache/yauzl-npm-3.2.0-22e6762265-a3cd2bfcf7.zip b/.yarn/cache/yauzl-npm-3.2.0-22e6762265-a3cd2bfcf7.zip new file mode 100644 index 00000000..cd764b3c Binary files /dev/null and b/.yarn/cache/yauzl-npm-3.2.0-22e6762265-a3cd2bfcf7.zip differ diff --git a/app/koi-server/docker-compose.yml b/app/koi-server/docker-compose.yml index 2302eafe..41ecc77a 100644 --- a/app/koi-server/docker-compose.yml +++ b/app/koi-server/docker-compose.yml @@ -33,7 +33,7 @@ services: - 8080:8080 environment: - DYNAMIC_CONFIG_ENABLED=true - - KAFKA_CLUSTERS_0_NAME=koi_kafka + - KAFKA_CLUSTERS_0_NAME=stock-service - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=kafka:9092 mongo: diff --git a/app/koi-server/jest.config.js b/app/koi-server/jest.config.js new file mode 100644 index 00000000..3ebae3fb --- /dev/null +++ b/app/koi-server/jest.config.js @@ -0,0 +1,11 @@ +module.exports = { + collectCoverageFrom: ['**/*.(t|j)s'], + coverageDirectory: '../coverage', + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: 'src', + testEnvironment: 'node', + testRegex: '.*\\.spec\\.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, +}; diff --git a/app/koi-server/package.json b/app/koi-server/package.json index e8ee0f9e..45bec220 100644 --- a/app/koi-server/package.json +++ b/app/koi-server/package.json @@ -10,21 +10,23 @@ "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest", - "test:watch": "jest --watch", - "test:cov": "jest --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test": "jest --config jest.config.js", + "test:watch": "jest --config jest.config.js --watch", + "test:cov": "jest --config jest.config.js --coverage", "test:e2e": "jest --config ./test/jest-e2e.json", "turbo:dev": "turbo run dev", "turbo:build": "turbo run build" }, "dependencies": { "@codegenie/serverless-express": "^4.13.0", + "@confluentinc/kafka-javascript": "^1.2.0", "@nestjs/common": "^10.3.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.3.0", + "@nestjs/microservices": "^11.0.7", "@nestjs/mongoose": "^10.0.2", "@nestjs/platform-express": "^10.3.0", + "@nestjs/schedule": "^5.0.1", "@toss/date": "^1.1.7", "@toss/utils": "^1.4.4", "aws-lambda": "^1.0.7", @@ -34,6 +36,7 @@ "express": "^4.18.2", "feature-nest-poll": "workspace:*", "feature-nest-stock": "workspace:*", + "kafkajs": "^2.2.4", "mongoose": "^8.1.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" diff --git a/app/koi-server/src/app/app.controller.ts b/app/koi-server/src/app/app.controller.ts index e35cefca..62a34809 100644 --- a/app/koi-server/src/app/app.controller.ts +++ b/app/koi-server/src/app/app.controller.ts @@ -1,7 +1,10 @@ -import { Controller, Post, Query } from '@nestjs/common'; +import { Controller, Get, Post, Query } from '@nestjs/common'; +import { AppService } from './app.service'; @Controller() export class AppController { + constructor(private readonly appService: AppService) {} + @Post('log') log(@Query('msg') msg: string): boolean { fetch( @@ -19,4 +22,9 @@ export class AppController { console.warn(`${decodeURIComponent(msg)} (${new Date()})`); return true; } + + @Get() + getHello(): string { + return this.appService.getHello(); + } } diff --git a/app/koi-server/src/app/app.module.ts b/app/koi-server/src/app/app.module.ts index b47b0c0a..73e3a97a 100644 --- a/app/koi-server/src/app/app.module.ts +++ b/app/koi-server/src/app/app.module.ts @@ -1,14 +1,23 @@ import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; +import { ConfigModule, ConfigService } from '@nestjs/config'; import { PollModule } from 'feature-nest-poll'; import { StockModule } from 'feature-nest-stock'; +import { ScheduleModule } from '@nestjs/schedule'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { PartyModule } from './party/party.module'; +import { KafkaModule } from '../../../../package/feature/feature-nest-stock/src/kafka/kafka.module'; +import kafkaConfig from '../config/kafka.config'; @Module({ controllers: [AppController], imports: [ + ScheduleModule.forRoot(), + ConfigModule.forRoot({ + isGlobal: true, + load: [kafkaConfig], + }), MongooseModule.forRoot(process.env.MONGO_URI, { ignoreUndefined: true, maxIdleTimeMS: 60000, @@ -16,6 +25,15 @@ import { PartyModule } from './party/party.module'; PollModule, StockModule, PartyModule, + KafkaModule.forRootAsync({ + imports: [ConfigModule], + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + brokers: configService.get('kafka.brokers') || [], + clientId: configService.get('kafka.clientId') || '', + groupId: configService.get('kafka.groupId') || '', + }), + }), ], providers: [AppService], }) diff --git a/app/koi-server/src/app/app.service.ts b/app/koi-server/src/app/app.service.ts index b4166f1d..927d7cca 100644 --- a/app/koi-server/src/app/app.service.ts +++ b/app/koi-server/src/app/app.service.ts @@ -2,5 +2,7 @@ import { Injectable } from '@nestjs/common'; @Injectable() export class AppService { - constructor() {} + getHello(): string { + return 'Hello World!'; + } } diff --git a/app/koi-server/src/config/kafka.config.ts b/app/koi-server/src/config/kafka.config.ts new file mode 100644 index 00000000..9b1e78a6 --- /dev/null +++ b/app/koi-server/src/config/kafka.config.ts @@ -0,0 +1,7 @@ +export default (): Record => ({ + kafka: { + brokers: process.env.KAFKA_BROKERS?.split(',') || ['localhost:9094'], + clientId: process.env.KAFKA_CLIENT_ID || 'stock-service', + groupId: process.env.KAFKA_GROUP_ID || 'stock_service_group', + }, +}); diff --git a/app/koi-server/test/app.e2e-spec.ts b/app/koi-server/test/app.e2e-spec.ts index 3ea33d9f..a243a9a6 100644 --- a/app/koi-server/test/app.e2e-spec.ts +++ b/app/koi-server/test/app.e2e-spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import * as request from 'supertest'; -import { AppModule } from '../src/app.module'; +import request from 'supertest'; +import { AppModule } from '../src/app/app.module'; describe('AppController (e2e)', () => { let app: INestApplication; diff --git a/package/feature/feature-nest-stock/jest.config.js b/package/feature/feature-nest-stock/jest.config.js new file mode 100644 index 00000000..cc743c87 --- /dev/null +++ b/package/feature/feature-nest-stock/jest.config.js @@ -0,0 +1,11 @@ +module.exports = { + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: '.', + testRegex: '.*\\.spec\\.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + collectCoverageFrom: ['**/*.(t|j)s'], + coverageDirectory: './coverage', + testEnvironment: 'node', +}; diff --git a/package/feature/feature-nest-stock/package.json b/package/feature/feature-nest-stock/package.json index a05eac1c..4d32e722 100644 --- a/package/feature/feature-nest-stock/package.json +++ b/package/feature/feature-nest-stock/package.json @@ -1,10 +1,17 @@ { "name": "feature-nest-stock", - "main": "./dist/index.js", + "main": "./dist/src/index.js", "sideEffects": false, "scripts": { "build": "tsc", - "build:watch": "tsc -w" + "build:watch": "tsc -w", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json", + "test:integration": "jest --testPathPattern=test/integration", + "test:outbox": "jest --testPathPattern=test/unit/outbox" }, "dependencies": { "@nestjs/common": "^10.3.0", @@ -12,11 +19,13 @@ "@nestjs/core": "^10.3.0", "@nestjs/mongoose": "^10.0.2", "@nestjs/platform-express": "^10.3.0", + "@nestjs/schedule": "^5.0.1", "@toss/date": "^1.1.7", "@toss/utils": "^1.4.4", "class-transformer": "^0.5.1", "cross-env": "^7.0.3", "dayjs": "^1.11.10", + "kafkajs": "^2.2.4", "lodash": "^4.17.21", "mongoose": "^8.1.0", "openai": "^4.85.1", @@ -48,6 +57,7 @@ "eslint-plugin-unused-imports": "^2.0.0", "jest": "29.5.0", "mongodb": "6.3.0", + "mongodb-memory-server": "^10.1.4", "prettier": "^2.6.1", "shared~type-stock": "workspace:*", "source-map-support": "^0.5.20", diff --git a/package/feature/feature-nest-stock/src/kafka/kafka.module.ts b/package/feature/feature-nest-stock/src/kafka/kafka.module.ts new file mode 100644 index 00000000..74804f47 --- /dev/null +++ b/package/feature/feature-nest-stock/src/kafka/kafka.module.ts @@ -0,0 +1,56 @@ +import { Module, DynamicModule, Type, ForwardReference, Abstract } from '@nestjs/common'; +import { KafkaService } from './kafka.service'; + +export interface KafkaModuleOptions { + brokers: string[]; + clientId: string; + groupId: string; +} + +type ModuleType = Type | DynamicModule | Promise | ForwardReference; + +export interface KafkaModuleAsyncOptions { + imports?: ModuleType[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + useFactory: (...args: any[]) => Promise | KafkaModuleOptions; + // eslint-disable-next-line @typescript-eslint/ban-types + inject?: Array | string | symbol | Abstract | Function>; +} + +@Module({ + exports: [KafkaService], + providers: [KafkaService], +}) +export class KafkaModule { + static forRoot(options: KafkaModuleOptions): DynamicModule { + return { + exports: [KafkaService], + global: true, + module: KafkaModule, + providers: [ + { + provide: 'KAFKA_OPTIONS', + useValue: options, + }, + KafkaService, + ], + }; + } + + static forRootAsync(options: KafkaModuleAsyncOptions): DynamicModule { + return { + exports: [KafkaService], + global: true, + imports: options.imports || [], + module: KafkaModule, + providers: [ + { + inject: options.inject || [], + provide: 'KAFKA_OPTIONS', + useFactory: options.useFactory, + }, + KafkaService, + ], + }; + } +} diff --git a/package/feature/feature-nest-stock/src/kafka/kafka.service.ts b/package/feature/feature-nest-stock/src/kafka/kafka.service.ts new file mode 100644 index 00000000..f0e8d62d --- /dev/null +++ b/package/feature/feature-nest-stock/src/kafka/kafka.service.ts @@ -0,0 +1,245 @@ +import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import { EachMessagePayload, Kafka, Producer, Consumer } from 'kafkajs'; + +@Injectable() +export class KafkaService implements OnModuleInit, OnModuleDestroy { + private readonly logger = new Logger(KafkaService.name); + + private kafka = new Kafka({ + brokers: ['localhost:9094'], + clientId: 'koi_kafka', + }); + + private producer: Producer; + + private consumer: Consumer; + + private isProducerConnected = false; + + private isConsumerConnected = false; + + private readonly maxRetries = 3; + + private readonly retryDelay = 1000; // ms + + constructor() { + this.producer = this.kafka.producer({ + allowAutoTopicCreation: true, + transactionTimeout: 30000, + }); + + this.consumer = this.kafka.consumer({ + groupId: 'stock_service_group', + heartbeatInterval: 3000, + sessionTimeout: 30000, + }); + } + + /** + * NestJS ๋ผ์ดํ”„์‚ฌ์ดํด ํ›… - ๋ชจ๋“ˆ์ด ์ดˆ๊ธฐํ™”๋  ๋•Œ ์ž๋™์œผ๋กœ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. + */ + async onModuleInit(): Promise { + await this.connectConsumer(); + await this.consumer.subscribe({ topics: ['stock.transaction.topic'] }); + await this.consumer.run({ + autoCommitInterval: 5000, + autoCommitThreshold: 100, + eachMessage: this.consumerCallback, + }); + this.logger.log('Kafka ์ปจ์Šˆ๋จธ๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์—ˆ๊ณ  ํ† ํ”ฝ ๊ตฌ๋…์„ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค.'); + } + + /** + * NestJS ๋ผ์ดํ”„์‚ฌ์ดํด ํ›… - ๋ชจ๋“ˆ์ด ์ข…๋ฃŒ๋  ๋•Œ ์ž๋™์œผ๋กœ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. + */ + async onModuleDestroy(): Promise { + await this.disconnectProducer(); + await this.disconnectConsumer(); + this.logger.log('Kafka ์—ฐ๊ฒฐ์ด ์•ˆ์ „ํ•˜๊ฒŒ ์ข…๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.'); + } + + /** + * Kafka ๋ฉ”์‹œ์ง€ ์ˆ˜์‹  ์ฝœ๋ฐฑ ํ•จ์ˆ˜ + */ + async consumerCallback(payload: EachMessagePayload): Promise { + try { + const messageValue = payload.message.value?.toString() || ''; + this.logger.debug(`Kafka ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ : ${payload.topic} - ${messageValue}`); + + // ์—ฌ๊ธฐ์— ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€ + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Kafka ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${errorMessage}`, errorStack); + } + } + + /** + * ์ถ”๊ฐ€ ํ† ํ”ฝ ๊ตฌ๋… ์„ค์ • + */ + async addSubscriptionTopic(topic: string): Promise { + try { + await this.consumer.pause([{ topic }]); + await this.consumer.subscribe({ topic }); + await this.consumer.resume([{ topic }]); + this.logger.log(`ํ† ํ”ฝ '${topic}'์— ๋Œ€ํ•œ ๊ตฌ๋…์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`ํ† ํ”ฝ '${topic}' ๊ตฌ๋… ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${errorMessage}`, errorStack); + throw error; + } + } + + /** + * Producer ์—ฐ๊ฒฐ ๊ด€๋ฆฌ - ์—ฐ๊ฒฐ์ด ์—†์œผ๋ฉด ์—ฐ๊ฒฐ ์ˆ˜๋ฆฝ + */ + private async connectProducer(): Promise { + if (!this.isProducerConnected) { + try { + await this.producer.connect(); + this.isProducerConnected = true; + this.logger.debug('Kafka ํ”„๋กœ๋“€์„œ ์—ฐ๊ฒฐ ์„ฑ๊ณต'); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Kafka ํ”„๋กœ๋“€์„œ ์—ฐ๊ฒฐ ์‹คํŒจ: ${errorMessage}`, errorStack); + throw error; + } + } + } + + /** + * Consumer ์—ฐ๊ฒฐ ๊ด€๋ฆฌ - ์—ฐ๊ฒฐ์ด ์—†์œผ๋ฉด ์—ฐ๊ฒฐ ์ˆ˜๋ฆฝ + */ + private async connectConsumer(): Promise { + if (!this.isConsumerConnected) { + try { + await this.consumer.connect(); + this.isConsumerConnected = true; + this.logger.debug('Kafka ์ปจ์Šˆ๋จธ ์—ฐ๊ฒฐ ์„ฑ๊ณต'); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Kafka ์ปจ์Šˆ๋จธ ์—ฐ๊ฒฐ ์‹คํŒจ: ${errorMessage}`, errorStack); + throw error; + } + } + } + + /** + * Producer ์—ฐ๊ฒฐ ์ข…๋ฃŒ + */ + private async disconnectProducer(): Promise { + if (this.isProducerConnected) { + try { + await this.producer.disconnect(); + this.isProducerConnected = false; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Kafka ํ”„๋กœ๋“€์„œ ์—ฐ๊ฒฐ ์ข…๋ฃŒ ์‹คํŒจ: ${errorMessage}`, errorStack); + throw error; + } + } + } + + /** + * Consumer ์—ฐ๊ฒฐ ์ข…๋ฃŒ + */ + private async disconnectConsumer(): Promise { + if (this.isConsumerConnected) { + try { + await this.consumer.disconnect(); + this.isConsumerConnected = false; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Kafka ์ปจ์Šˆ๋จธ ์—ฐ๊ฒฐ ์ข…๋ฃŒ ์‹คํŒจ: ${errorMessage}`, errorStack); + throw error; + } + } + } + + /** + * ๋‹จ์ผ ๋ฉ”์‹œ์ง€ ์ „์†ก - ๊ธฐ์กด API ํ˜ธํ™˜์„ฑ ์œ ์ง€ + */ + async sendMessage(topic: string, message: unknown): Promise { + let retries = 0; + let success = false; + + while (!success && retries < this.maxRetries) { + try { + await this.connectProducer(); + await this.producer.send({ + messages: [{ value: JSON.stringify(message) }], + topic, + }); + success = true; + this.logger.debug(`Kafka ๋ฉ”์‹œ์ง€ ์ „์†ก ์„ฑ๊ณต: ${topic}`); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + retries++; + this.logger.warn(`Kafka ๋ฉ”์‹œ์ง€ ์ „์†ก ์žฌ์‹œ๋„ (${retries}/${this.maxRetries}): ${errorMessage}`, errorStack); + + // ์—ฐ๊ฒฐ ๋ฌธ์ œ์ผ ๊ฒฝ์šฐ ์—ฐ๊ฒฐ ์žฌ์„ค์ • + this.isProducerConnected = false; + + if (retries >= this.maxRetries) { + this.logger.error(`Kafka ๋ฉ”์‹œ์ง€ ์ „์†ก ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ: ${errorMessage}`, errorStack); + throw error; + } + + // ์žฌ์‹œ๋„ ์ „ ์ง€์—ฐ + await new Promise((resolve) => { + setTimeout(resolve, this.retryDelay); + }); + } + } + } + + /** + * ๋ฐฐ์น˜ ๋ฉ”์‹œ์ง€ ์ „์†ก (์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ) + */ + async sendBatchMessages(topic: string, messages: unknown[]): Promise { + if (!messages.length) { + return; + } + + let retries = 0; + let success = false; + + while (!success && retries < this.maxRetries) { + try { + await this.connectProducer(); + await this.producer.send({ + messages: messages.map((message) => ({ + value: JSON.stringify(message), + })), + topic, + }); + success = true; + this.logger.debug(`Kafka ๋ฐฐ์น˜ ๋ฉ”์‹œ์ง€ ์ „์†ก ์„ฑ๊ณต: ${topic} (${messages.length}๊ฐœ)`); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + retries++; + this.logger.warn(`Kafka ๋ฐฐ์น˜ ๋ฉ”์‹œ์ง€ ์ „์†ก ์žฌ์‹œ๋„ (${retries}/${this.maxRetries}): ${errorMessage}`, errorStack); + + // ์—ฐ๊ฒฐ ๋ฌธ์ œ์ผ ๊ฒฝ์šฐ ์—ฐ๊ฒฐ ์žฌ์„ค์ • + this.isProducerConnected = false; + + if (retries >= this.maxRetries) { + this.logger.error(`Kafka ๋ฐฐ์น˜ ๋ฉ”์‹œ์ง€ ์ „์†ก ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ: ${errorMessage}`, errorStack); + throw error; + } + + // ์žฌ์‹œ๋„ ์ „ ์ง€์—ฐ + await new Promise((resolve) => { + setTimeout(resolve, this.retryDelay); + }); + } + } + } +} diff --git a/package/feature/feature-nest-stock/src/outbox/outbox.module.ts b/package/feature/feature-nest-stock/src/outbox/outbox.module.ts new file mode 100644 index 00000000..6726b796 --- /dev/null +++ b/package/feature/feature-nest-stock/src/outbox/outbox.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { Outbox, OutboxSchema } from './outbox.schema'; +import { OutboxRepository } from './outbox.repository'; +import { OutboxProcessor } from './outbox.processor'; +import { KafkaModule } from '../kafka/kafka.module'; +import { OutboxService } from './outbox.service'; + +@Module({ + exports: [OutboxRepository, OutboxService], + imports: [MongooseModule.forFeature([{ name: Outbox.name, schema: OutboxSchema }]), KafkaModule], + providers: [OutboxRepository, OutboxProcessor, OutboxService], +}) +export class OutboxModule {} diff --git a/package/feature/feature-nest-stock/src/outbox/outbox.processor.ts b/package/feature/feature-nest-stock/src/outbox/outbox.processor.ts new file mode 100644 index 00000000..2de26885 --- /dev/null +++ b/package/feature/feature-nest-stock/src/outbox/outbox.processor.ts @@ -0,0 +1,129 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { KafkaService } from '../kafka/kafka.service'; +import { OutboxService } from './outbox.service'; +import { OutboxStatus } from './outbox.schema'; + +@Injectable() +export class OutboxProcessor { + private readonly logger = new Logger(OutboxProcessor.name); + + private readonly MAX_RETRIES = 3; + + // ๋งˆ์ง€๋ง‰ ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ์ถ”์ ์„ ์œ„ํ•œ ๋ณ€์ˆ˜ ์ถ”๊ฐ€ + private lastProcessingTime = Date.now(); + + private readonly IDLE_CHECK_INTERVAL = 60000; // 60์ดˆ (1๋ถ„) + + private isProcessingActive = true; // ์ฒ˜๋ฆฌ ํ™œ์„ฑํ™” ์ƒํƒœ ํ”Œ๋ž˜๊ทธ + + constructor(private readonly outboxService: OutboxService, private readonly kafkaService: KafkaService) {} + + @Cron(CronExpression.EVERY_5_SECONDS) + async processOutboxMessages(): Promise { + // ์ฒ˜๋ฆฌ๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋œ ์ƒํƒœ์ด๋ฉด ๋จผ์ € ํ™•์ธ + if (!this.isProcessingActive) { + // ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ์—์„œ๋Š” 1๋ถ„๋งˆ๋‹ค๋งŒ DB ํ™•์ธ + const timeSinceLastCheck = Date.now() - this.lastProcessingTime; + if (timeSinceLastCheck < this.IDLE_CHECK_INTERVAL) { + return; // ์•„์ง ์ฒดํฌ ์‹œ๊ฐ„์ด ๋˜์ง€ ์•Š์•˜์œผ๋ฉด ๋ฐ”๋กœ ์ข…๋ฃŒ + } + } + + try { + // ๋จผ์ € ์ฒ˜๋ฆฌํ•  ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋Š”์ง€ ๋น ๋ฅด๊ฒŒ ํ™•์ธ + const hasPendingMessages = await this.outboxService.hasPendingMessages(); + + if (!hasPendingMessages) { + // ์ฒ˜๋ฆฌํ•  ๋ฉ”์‹œ์ง€๊ฐ€ ์—†์œผ๋ฉด ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ๋กœ ์ „ํ™˜ + if (this.isProcessingActive) { + this.logger.log('์ฒ˜๋ฆฌํ•  ๋ฉ”์‹œ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์„ธ์„œ๋ฅผ ๋Œ€๊ธฐ ์ƒํƒœ๋กœ ์ „ํ™˜ํ•ฉ๋‹ˆ๋‹ค.'); + this.isProcessingActive = false; + } + this.lastProcessingTime = Date.now(); + return; + } + + // ์ฒ˜๋ฆฌํ•  ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ์œผ๋ฉด ํ™œ์„ฑํ™” ์ƒํƒœ๋กœ ์ „ํ™˜ + if (!this.isProcessingActive) { + this.logger.log('์ฒ˜๋ฆฌํ•  ๋ฉ”์‹œ์ง€๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์„ธ์„œ๋ฅผ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค.'); + this.isProcessingActive = true; + } + + // ์‹ค์ œ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ง„ํ–‰ + const pendingEvents = await this.outboxService.getPendingMessages(10); + this.lastProcessingTime = Date.now(); + + for (const event of pendingEvents) { + try { + this.logger.debug(`Processing outbox event: ${event._id}, type: ${event.eventType}`); + + await this.kafkaService.sendMessage(event.topic, JSON.parse(event.payload)); + await this.outboxService.updateOutboxStatus(event._id, OutboxStatus.PROCESSED); + + this.logger.debug(`Successfully processed outbox event: ${event._id}`); + } catch (error: unknown) { + const errorStack = error instanceof Error ? error.stack : undefined; + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + + this.logger.error(`Failed to process outbox event: ${event._id}`, errorStack); + await this.outboxService.updateOutboxStatus(event._id, OutboxStatus.FAILED, errorMessage); + } + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Error in outbox processing: ${errorMessage}`, errorStack); + } + } + + @Cron(CronExpression.EVERY_30_SECONDS) + async retryFailedMessages(): Promise { + // ์ฒ˜๋ฆฌ๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋œ ์ƒํƒœ์ด๋ฉด ํ™•์ธ ์—†์ด ์ข…๋ฃŒ + if (!this.isProcessingActive) { + const timeSinceLastCheck = Date.now() - this.lastProcessingTime; + if (timeSinceLastCheck < this.IDLE_CHECK_INTERVAL) { + return; // ์•„์ง ์ฒดํฌ ์‹œ๊ฐ„์ด ๋˜์ง€ ์•Š์•˜์œผ๋ฉด ๋ฐ”๋กœ ์ข…๋ฃŒ + } + } + + try { + // ๋จผ์ € ์žฌ์‹œ๋„ํ•  ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋Š”์ง€ ๋น ๋ฅด๊ฒŒ ํ™•์ธ + const hasFailedMessages = await this.outboxService.hasFailedMessages(this.MAX_RETRIES); + + if (!hasFailedMessages) { + return; // ์ฒ˜๋ฆฌํ•  ๋ฉ”์‹œ์ง€๊ฐ€ ์—†์œผ๋ฉด ๋ฐ”๋กœ ์ข…๋ฃŒ + } + + // ์žฌ์‹œ๋„ํ•  ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ์œผ๋ฉด ํ™œ์„ฑํ™” ์ƒํƒœ๋กœ ์ „ํ™˜ + if (!this.isProcessingActive) { + this.logger.log('์žฌ์‹œ๋„ํ•  ์‹คํŒจ ๋ฉ”์‹œ์ง€๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ํ”„๋กœ์„ธ์„œ๋ฅผ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค.'); + this.isProcessingActive = true; + } + + const failedEvents = await this.outboxService.getFailedMessages(this.MAX_RETRIES); + this.lastProcessingTime = Date.now(); + + for (const event of failedEvents) { + try { + this.logger.debug(`Retrying failed outbox event: ${event._id}, attempt: ${event.retryCount + 1}`); + + await this.kafkaService.sendMessage(event.topic, JSON.parse(event.payload)); + await this.outboxService.updateOutboxStatus(event._id, OutboxStatus.PROCESSED); + + this.logger.debug(`Successfully processed failed outbox event: ${event._id}`); + } catch (error: unknown) { + const errorStack = error instanceof Error ? error.stack : undefined; + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + + this.logger.error(`Failed to retry outbox event: ${event._id}`, errorStack); + await this.outboxService.updateOutboxStatus(event._id, OutboxStatus.FAILED, errorMessage); + } + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Error in failed outbox processing: ${errorMessage}`, errorStack); + } + } +} diff --git a/package/feature/feature-nest-stock/src/outbox/outbox.repository.ts b/package/feature/feature-nest-stock/src/outbox/outbox.repository.ts new file mode 100644 index 00000000..e64f6ba6 --- /dev/null +++ b/package/feature/feature-nest-stock/src/outbox/outbox.repository.ts @@ -0,0 +1,94 @@ +import { Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, QueryOptions } from 'mongoose'; +import { Outbox, OutboxDocument, OutboxEventType, OutboxStatus } from './outbox.schema'; + +@Injectable() +export class OutboxRepository { + constructor(@InjectModel(Outbox.name) private readonly outboxModel: Model) {} + + async create( + eventType: OutboxEventType, + payload: unknown, + topic: string, + options?: QueryOptions, + ): Promise { + return this.outboxModel + .create( + [ + { + eventType, + payload: JSON.stringify(payload), + retryCount: 0, + status: OutboxStatus.PENDING, + topic, + }, + ], + options, + ) + .then((docs) => docs[0]); + } + + async findPendingEvents(limit = 10): Promise { + return this.outboxModel.find({ status: OutboxStatus.PENDING }).sort({ createdAt: 1 }).limit(limit).exec(); + } + + /** + * ๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€๋งŒ ํ™•์ธํ•˜๋Š” ์ตœ์ ํ™”๋œ ๋ฉ”์„œ๋“œ + * (์ „์ฒด ๋ฌธ์„œ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋Œ€์‹  ์นด์šดํŠธ๋งŒ ํ™•์ธํ•˜์—ฌ DB ๋ถ€ํ•˜ ์ตœ์†Œํ™”) + * @returns ๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ”์‹œ์ง€ ์กด์žฌ ์—ฌ๋ถ€ + */ + async hasPendingEvents(): Promise { + // countDocuments ๋Œ€์‹  exists ์‚ฌ์šฉํ•˜์—ฌ ๋” ๋น ๋ฅธ ํ™•์ธ (MongoDB ์ตœ์ ํ™”) + const result = await this.outboxModel.exists({ status: OutboxStatus.PENDING }); + return result !== null; + } + + async markAsProcessed(id: string): Promise { + return this.outboxModel.findByIdAndUpdate( + id, + { + processedAt: new Date(), + status: OutboxStatus.PROCESSED, + }, + { new: true }, + ); + } + + async markAsFailed(id: string, errorMessage: string): Promise { + return this.outboxModel.findByIdAndUpdate( + id, + { + $inc: { retryCount: 1 }, + errorMessage, + status: OutboxStatus.FAILED, + }, + { new: true }, + ); + } + + async findFailedEvents(maxRetries = 3): Promise { + return this.outboxModel + .find({ + retryCount: { $lt: maxRetries }, + status: OutboxStatus.FAILED, + }) + .sort({ updatedAt: 1 }) + .exec(); + } + + /** + * ์žฌ์‹œ๋„ ๊ฐ€๋Šฅํ•œ ์‹คํŒจ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€๋งŒ ํ™•์ธํ•˜๋Š” ์ตœ์ ํ™”๋œ ๋ฉ”์„œ๋“œ + * (์ „์ฒด ๋ฌธ์„œ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋Œ€์‹  ์นด์šดํŠธ๋งŒ ํ™•์ธํ•˜์—ฌ DB ๋ถ€ํ•˜ ์ตœ์†Œํ™”) + * @param maxRetries ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ + * @returns ์žฌ์‹œ๋„ ๊ฐ€๋Šฅํ•œ ์‹คํŒจ ๋ฉ”์‹œ์ง€ ์กด์žฌ ์—ฌ๋ถ€ + */ + async hasFailedEvents(maxRetries = 3): Promise { + // countDocuments ๋Œ€์‹  exists ์‚ฌ์šฉํ•˜์—ฌ ๋” ๋น ๋ฅธ ํ™•์ธ (MongoDB ์ตœ์ ํ™”) + const result = await this.outboxModel.exists({ + retryCount: { $lt: maxRetries }, + status: OutboxStatus.FAILED, + }); + return result !== null; + } +} diff --git a/package/feature/feature-nest-stock/src/outbox/outbox.schema.ts b/package/feature/feature-nest-stock/src/outbox/outbox.schema.ts new file mode 100644 index 00000000..088eb649 --- /dev/null +++ b/package/feature/feature-nest-stock/src/outbox/outbox.schema.ts @@ -0,0 +1,42 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; + +export type OutboxDocument = Outbox & Document; + +export enum OutboxStatus { + PENDING = 'PENDING', + PROCESSED = 'PROCESSED', + FAILED = 'FAILED', +} + +export enum OutboxEventType { + STOCK_PURCHASED = 'STOCK_PURCHASED', + STOCK_SOLD = 'STOCK_SOLD', + STOCK_INFO_DRAWN = 'STOCK_INFO_DRAWN', +} + +@Schema({ timestamps: true }) +export class Outbox { + @Prop({ required: true }) + eventType: OutboxEventType; + + @Prop({ required: true }) + payload: string; + + @Prop({ default: OutboxStatus.PENDING, required: true }) + status: OutboxStatus; + + @Prop({ required: true }) + topic: string; + + @Prop() + errorMessage?: string; + + @Prop({ default: 0 }) + retryCount: number; + + @Prop() + processedAt?: Date; +} + +export const OutboxSchema = SchemaFactory.createForClass(Outbox); diff --git a/package/feature/feature-nest-stock/src/outbox/outbox.service.ts b/package/feature/feature-nest-stock/src/outbox/outbox.service.ts new file mode 100644 index 00000000..fd298007 --- /dev/null +++ b/package/feature/feature-nest-stock/src/outbox/outbox.service.ts @@ -0,0 +1,305 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectConnection } from '@nestjs/mongoose'; +import { Connection, QueryOptions } from 'mongoose'; +import { Cron } from '@nestjs/schedule'; +import { OutboxRepository } from './outbox.repository'; +import { OutboxDocument, OutboxEventType, OutboxStatus } from './outbox.schema'; +import { KafkaService } from '../kafka/kafka.service'; + +@Injectable() +export class OutboxService { + private readonly logger = new Logger(OutboxService.name); + + constructor( + @InjectConnection() private readonly connection: Connection, + private readonly outboxRepository: OutboxRepository, + private readonly kafkaService: KafkaService, + ) {} + + /** + * ์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (๋ฉ”์‹œ์ง€ ์ €์žฅ๋งŒ ๋‹ด๋‹น, ์ „์†ก์€ ์™„์ „ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌ). + * @param eventType ์ด๋ฒคํŠธ ํƒ€์ž… + * @param payload ๋ฉ”์‹œ์ง€ ํŽ˜์ด๋กœ๋“œ + * @param topic Kafka ํ† ํ”ฝ + * @param options Mongoose ์ฟผ๋ฆฌ ์˜ต์…˜ (ํŠธ๋žœ์žญ์…˜ ์„ธ์…˜ ๋“ฑ) + */ + async createOutboxMessage( + eventType: OutboxEventType, + payload: unknown, + topic: string, + options?: QueryOptions, + ): Promise { + try { + // ์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€ ์ €์žฅ๋งŒ ์‹คํ–‰ (ํŠธ๋žœ์žญ์…˜ ๋ฝ ์ตœ์†Œํ™”) + return await this.outboxRepository.create(eventType, payload, topic, options); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Failed to create outbox message: ${errorMessage}`, errorStack); + throw error; + } + } + + /** + * ํŠน์ • ์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€์˜ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. + * @param id ์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€ ID + * @param status ์ƒˆ๋กœ์šด ์ƒํƒœ + * @param errorMessage ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ (์‹คํŒจ ์‹œ) + */ + async updateOutboxStatus( + id: string, + status: OutboxStatus, + errorMessage?: string, + ): Promise { + try { + if (status === OutboxStatus.PROCESSED) { + return await this.outboxRepository.markAsProcessed(id); + } + if (status === OutboxStatus.FAILED) { + return await this.outboxRepository.markAsFailed(id, errorMessage); + } + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Failed to update outbox status: ${errorMessage}`, errorStack); + throw error; + } + return undefined; + } + + /** + * ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * @param limit ์ตœ๋Œ€ ์กฐํšŒ ๊ฐœ์ˆ˜ + */ + async getPendingMessages(limit = 10): Promise { + try { + return await this.outboxRepository.findPendingEvents(limit); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Failed to get pending messages: ${errorMessage}`, errorStack); + throw error; + } + } + + /** + * ์‹คํŒจํ•œ ์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * @param maxRetries ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ + */ + async getFailedMessages(maxRetries = 3): Promise { + try { + return await this.outboxRepository.findFailedEvents(maxRetries); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Failed to get failed messages: ${errorMessage}`, errorStack); + throw error; + } + } + + /** + * ๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋Š”์ง€ ๋น ๋ฅด๊ฒŒ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ์„ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๊ณ  ์กด์žฌ ์—ฌ๋ถ€๋งŒ ํ™•์ธํ•˜์—ฌ ์„ฑ๋Šฅ ์ตœ์ ํ™” + * @returns ๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€ (boolean) + */ + async hasPendingMessages(): Promise { + try { + // limit 1๋กœ ์„ค์ •ํ•˜์—ฌ ์กด์žฌ ์—ฌ๋ถ€๋งŒ ๋น ๋ฅด๊ฒŒ ํ™•์ธ + const pendingEvents = await this.outboxRepository.findPendingEvents(1); + return pendingEvents.length > 0; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Failed to check pending messages: ${errorMessage}`, errorStack); + return false; // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์•ˆ์ „ํ•˜๊ฒŒ false ๋ฐ˜ํ™˜ + } + } + + /** + * ์žฌ์‹œ๋„ ๊ฐ€๋Šฅํ•œ ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋Š”์ง€ ๋น ๋ฅด๊ฒŒ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ์„ ๊ฐ€์ ธ์˜ค์ง€ ์•Š๊ณ  ์กด์žฌ ์—ฌ๋ถ€๋งŒ ํ™•์ธํ•˜์—ฌ ์„ฑ๋Šฅ ์ตœ์ ํ™” + * @param maxRetries ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜ + * @returns ์žฌ์‹œ๋„ ๊ฐ€๋Šฅํ•œ ์‹คํŒจ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€ (boolean) + */ + async hasFailedMessages(maxRetries = 3): Promise { + try { + // limit 1๋กœ ์„ค์ •ํ•˜์—ฌ ์กด์žฌ ์—ฌ๋ถ€๋งŒ ๋น ๋ฅด๊ฒŒ ํ™•์ธ + const failedEvents = await this.outboxRepository.findFailedEvents(maxRetries); + return failedEvents.length > 0; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`Failed to check failed messages: ${errorMessage}`, errorStack); + return false; // ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์•ˆ์ „ํ•˜๊ฒŒ false ๋ฐ˜ํ™˜ + } + } + + /** + * ๋ฐฐ์น˜ ์‚ฌ์ด์ฆˆ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•„์ˆ˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๊ฐ€์ง„ ๋ฉ”์†Œ๋“œ + * @param batchSize ํ•œ ๋ฒˆ์— ์ฒ˜๋ฆฌํ•  ๋ฉ”์‹œ์ง€ ์ˆ˜ + */ + private async processMessages(batchSize: number): Promise { + // ์ฒ˜๋ฆฌ ๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ”์‹œ์ง€ ์กฐํšŒ + const pendingMessages = await this.getPendingMessages(batchSize); + + if (pendingMessages.length === 0) { + return; + } + + this.logger.log(`์ฒ˜๋ฆฌํ•  ๋ฉ”์‹œ์ง€ ${pendingMessages.length}๊ฐœ ๋ฐœ๊ฒฌ`); + + // ํ† ํ”ฝ๋ณ„๋กœ ๋ฉ”์‹œ์ง€ ๊ทธ๋ฃนํ™” + const messagesByTopic: Record> = {}; + + // ๋ฉ”์‹œ์ง€ ์ค€๋น„ ๋ฐ ๊ทธ๋ฃนํ™” + pendingMessages.forEach((message) => { + try { + const kafkaMessage = { + eventType: message.eventType, + messageId: message._id.toString(), + payload: JSON.parse(message.payload as string), + timestamp: new Date().toISOString(), + }; + + // ํ† ํ”ฝ๋ณ„๋กœ ๋ฉ”์‹œ์ง€ ๊ทธ๋ฃนํ™” + if (!messagesByTopic[message.topic]) { + messagesByTopic[message.topic] = []; + } + messagesByTopic[message.topic].push({ kafkaMessage, message }); + } catch (error) { + // JSON ํŒŒ์‹ฑ ์—๋Ÿฌ ๋“ฑ ์ „์ฒ˜๋ฆฌ ์‹คํŒจ ์‹œ ๋ฐ”๋กœ ์‹คํŒจ ์ฒ˜๋ฆฌ + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + this.logger.error(`๋ฉ”์‹œ์ง€ ์ „์ฒ˜๋ฆฌ ์‹คํŒจ (ID: ${message._id}): ${errorMessage}`); + this.updateOutboxStatus(message._id.toString(), OutboxStatus.FAILED, errorMessage); + } + }); + + // ํ† ํ”ฝ๋ณ„๋กœ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ + const topicPromises = Object.entries(messagesByTopic).map(async ([topic, items]) => { + // ์นดํ”„์นด ๋ฉ”์‹œ์ง€ ๋ฐฐ์—ด ๋ฐ ๋ฉ”์‹œ์ง€ ID ๋งคํ•‘ + const kafkaMessages = items.map((item) => item.kafkaMessage); + const messageIds = items.map((item) => item.message._id.toString()); + + try { + // ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‚ฌ์šฉ + await this.kafkaService.sendBatchMessages(topic, kafkaMessages); + + // ์„ฑ๊ณตํ•œ ๋ฉ”์‹œ์ง€๋“ค ์ƒํƒœ ์—…๋ฐ์ดํŠธ + const updatePromises = messageIds.map((id) => this.updateOutboxStatus(id, OutboxStatus.PROCESSED)); + await Promise.all(updatePromises); + + this.logger.debug(`[${topic}] ${messageIds.length}๊ฐœ ๋ฉ”์‹œ์ง€ ๋ฐฐ์น˜ ์ „์†ก ์„ฑ๊ณต`); + return true; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + // ๋ฐฐ์น˜ ์ „์†ก ์‹คํŒจ ์‹œ, ๊ฐœ๋ณ„ ๋ฉ”์‹œ์ง€ ์ „์†ก ์‹œ๋„ (ํšŒ๋ณต์„ฑ ์ฆ๊ฐ€) + this.logger.warn(`[${topic}] ๋ฐฐ์น˜ ์ „์†ก ์‹คํŒจ, ๊ฐœ๋ณ„ ์ „์†ก ์‹œ๋„ ์ค‘: ${errorMessage}`, errorStack); + + // ๊ฐœ๋ณ„ ๋ฉ”์‹œ์ง€ ์ „์†ก ์‹œ๋„ + const individualPromises = items.map(async ({ message, kafkaMessage }) => { + try { + await this.kafkaService.sendMessage(topic, kafkaMessage); + await this.updateOutboxStatus(message._id.toString(), OutboxStatus.PROCESSED); + this.logger.debug(`๊ฐœ๋ณ„ ์ „์†ก ์„ฑ๊ณต: ${message.eventType}, ID: ${message._id}`); + return true; + } catch (innerError) { + const errorMessage = innerError instanceof Error ? innerError.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + await this.updateOutboxStatus(message._id.toString(), OutboxStatus.FAILED, errorMessage); + this.logger.error(`๊ฐœ๋ณ„ ์ „์†ก ์‹คํŒจ: ${message.eventType}, ID: ${message._id}`, innerError); + return false; + } + }); + + await Promise.all(individualPromises); + return false; + } + }); + + // ๋ชจ๋“  ํ† ํ”ฝ์˜ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ๋Œ€๊ธฐ + await Promise.all(topicPromises); + } + + /** + * 3์ดˆ๋งˆ๋‹ค ์‹คํ–‰๋˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ - ์ฒ˜๋ฆฌ ๋Œ€๊ธฐ ์ƒํƒœ์˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * ๊ณ ๋ถ€ํ•˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋” ์ ์€ ์ฃผ๊ธฐ(์˜ˆ: 5์ดˆ)๋กœ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + */ + @Cron('*/3 * * * * *') + async processPendingOutboxMessages(): Promise { + try { + const startTime = Date.now(); + await this.processMessages(50); // ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ ํ–ฅ์ƒ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ๋Ÿ‰ ์ฆ๊ฐ€ + const processingTime = Date.now() - startTime; + + // ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋กœ๊น… (๊ณ ๋ถ€ํ•˜ ๋ถ„์„์šฉ) + if (processingTime > 5000) { + // 5์ดˆ ์ด์ƒ ๊ฑธ๋ฆฌ๋ฉด ๊ฒฝ๊ณ  + this.logger.warn(`๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ์— ${processingTime}ms ์†Œ์š”๋จ (์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ๊ณผ๋‹ค)`); + } else { + this.logger.debug(`๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์™„๋ฃŒ: ${processingTime}ms ์†Œ์š”`); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${errorMessage}`, errorStack); + } + } + + /** + * 1๋ถ„๋งˆ๋‹ค ์‹คํ–‰๋˜๋Š” ์Šค์ผ€์ค„๋Ÿฌ - ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ์žฌ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. + * ์‹คํŒจ ๋ฉ”์‹œ์ง€๋Š” ์ ์ง„์ ์ธ ๋ฐฑ์˜คํ”„๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ดˆ๊ณผํ•œ ๋ฉ”์‹œ์ง€๋Š” DLQ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ + @Cron('0 */1 * * * *') + async retryFailedOutboxMessages(): Promise { + try { + // ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜(10๋ฒˆ)์„ ์ดˆ๊ณผํ•˜์ง€ ์•Š์€ ์‹คํŒจ ๋ฉ”์‹œ์ง€๋งŒ ์กฐํšŒ + const failedMessages = await this.getFailedMessages(10); + + if (failedMessages.length === 0) { + return; + } + + this.logger.log(`์žฌ์‹œ๋„ํ•  ์‹คํŒจ ๋ฉ”์‹œ์ง€ ${failedMessages.length}๊ฐœ ๋ฐœ๊ฒฌ`); + + // ์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ๊ทธ๋ฃนํ™” (์žฌ์‹œ๋„ ํšŸ์ˆ˜์— ๋”ฐ๋ผ) + const messagesByRetryCount: Record = {}; + + failedMessages.forEach((message) => { + const retryCount = message.retryCount || 0; + if (!messagesByRetryCount[retryCount]) { + messagesByRetryCount[retryCount] = []; + } + messagesByRetryCount[retryCount].push(message); + }); + + // ์žฌ์‹œ๋„ ํšŸ์ˆ˜์— ๋”ฐ๋ผ ์ ์ง„์ ์œผ๋กœ ์ฒ˜๋ฆฌ (๋ฐฑ์˜คํ”„ ์ „๋žต) + for (const [retryCountStr, messages] of Object.entries(messagesByRetryCount)) { + const retryCount = Number(retryCountStr); + + // ์žฌ์‹œ๋„ ์ง€์—ฐ ์‹œ๊ฐ„ (์ง€์ˆ˜ ๋ฐฑ์˜คํ”„) + // ์žฌ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ฆ๊ฐ€ํ• ์ˆ˜๋ก ์ง€์—ฐ ์‹œ๊ฐ„ ์ฆ๊ฐ€ (1์ดˆ, 2์ดˆ, 4์ดˆ, 8์ดˆ...) + const delayMs = Math.min(1000 * 2 ** retryCount, 60000); // ์ตœ๋Œ€ 1๋ถ„ + + this.logger.debug(`์žฌ์‹œ๋„ ํšŸ์ˆ˜ ${retryCount}ํšŒ์ธ ๋ฉ”์‹œ์ง€ ${messages.length}๊ฐœ ์ฒ˜๋ฆฌ ์ค‘... (์ง€์—ฐ: ${delayMs}ms)`); + + if (delayMs > 0 && retryCount > 0) { + // ์ ์ง„์  ์ง€์—ฐ ์ฒ˜๋ฆฌ + await new Promise((resolve) => { + setTimeout(resolve, delayMs); + }); + } + + // ์žฌ์‹œ๋„๋ฅผ ์œ„ํ•ด PENDING ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ + const updatePromises = messages.map((message) => + this.updateOutboxStatus(message._id.toString(), OutboxStatus.PENDING), + ); + + await Promise.all(updatePromises); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜'; + const errorStack = error instanceof Error ? error.stack : undefined; + this.logger.error(`์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€ ์žฌ์‹œ๋„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${errorMessage}`, errorStack); + } + } +} diff --git a/package/feature/feature-nest-stock/src/stock.module.ts b/package/feature/feature-nest-stock/src/stock.module.ts index ba3f5dc6..fa8bb72b 100644 --- a/package/feature/feature-nest-stock/src/stock.module.ts +++ b/package/feature/feature-nest-stock/src/stock.module.ts @@ -1,12 +1,14 @@ import { Module, forwardRef } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; -import { StockService } from './stock.service'; import { Stock, stockSchema } from './stock.schema'; import { StockController } from './stock.controller'; +import { StockService } from './stock.service'; import { LogModule } from './log/log.module'; import { ResultModule } from './result/result.module'; import { StockRepository } from './stock.repository'; import { UserModule } from './user/user.module'; +import { OutboxModule } from './outbox/outbox.module'; +import { KafkaService } from './kafka/kafka.service'; @Module({ controllers: [StockController], @@ -16,7 +18,8 @@ import { UserModule } from './user/user.module'; forwardRef(() => UserModule), LogModule, ResultModule, + OutboxModule, ], - providers: [StockService, StockRepository], + providers: [StockService, StockRepository, KafkaService], }) export class StockModule {} diff --git a/package/feature/feature-nest-stock/src/stock.service.ts b/package/feature/feature-nest-stock/src/stock.service.ts index 32139e5c..080cf26d 100644 --- a/package/feature/feature-nest-stock/src/stock.service.ts +++ b/package/feature/feature-nest-stock/src/stock.service.ts @@ -14,6 +14,9 @@ import { ResultService } from './result/result.service'; import { Result } from './result/result.schema'; import { StockRepository } from './stock.repository'; import { UserRepository } from './user/user.repository'; +import { OutboxService } from './outbox/outbox.service'; +import { OutboxEventType } from './outbox/outbox.schema'; +import { KafkaService } from './kafka/kafka.service'; @Injectable() export class StockService { @@ -26,6 +29,8 @@ export class StockService { private readonly userService: UserService, private readonly logService: LogService, private readonly resultService: ResultService, + private readonly outboxService: OutboxService, + private readonly kafkaService: KafkaService, ) {} transStockToDto(stock: StockDocument): Response.GetStock { @@ -253,6 +258,7 @@ export class StockService { await user.save({ session }); result = await stock.save({ session }); + // ๋กœ๊ทธ ์ƒ์„ฑ stockLog = new StockLog({ action: 'BUY', company, @@ -262,6 +268,36 @@ export class StockService { stockId, userId, }); + + console.debug({ + amount, + company, + stockId, + timestamp: new Date().toISOString(), + totalPrice, + unitPrice: companyPrice, + userId, + }); + + // ํŠธ๋žœ์žญ์…˜ ์„ฑ๊ณต ์‹œ Outbox ํŒจํ„ด์„ ํ†ตํ•ด ๋ฉ”์‹œ์ง€ ์ ์žฌ + try { + const payload = { + ...stockLog, + timestamp: new Date().toISOString(), + }; + + await this.outboxService.createOutboxMessage( + OutboxEventType.STOCK_PURCHASED, + payload, + 'stock.transaction.topic', + { session }, + ); + + console.debug('Outbox ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ์„ฑ๊ณต:', OutboxEventType.STOCK_PURCHASED); + } catch (outboxError) { + console.error('Outbox ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ์‹คํŒจ:', outboxError); + // Outbox ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ์‹คํŒจ๋Š” ์ „์ฒด ํŠธ๋žœ์žญ์…˜์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Œ + } }); } catch (error) { console.error(error); @@ -270,6 +306,7 @@ export class StockService { await session.endSession(); } + // ํŠธ๋žœ์žญ์…˜ ์™ธ๋ถ€์—์„œ ๋กœ๊ทธ ์ €์žฅ await this.logService.addLog(stockLog); return result; @@ -440,6 +477,28 @@ export class StockService { stockId, userId, }); + + // ํŠธ๋žœ์žญ์…˜ ์„ฑ๊ณต ์‹œ Outbox ํŒจํ„ด์„ ํ†ตํ•ด ๋ฉ”์‹œ์ง€ ์ ์žฌ + try { + const payload = { + amount, + company, + stockId, + timestamp: new Date().toISOString(), + totalPrice, + unitPrice: companyPrice, + userId, + }; + + await this.outboxService.createOutboxMessage(OutboxEventType.STOCK_SOLD, payload, 'stock.transaction.topic', { + session, + }); + + console.debug('Outbox ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ์„ฑ๊ณต:', OutboxEventType.STOCK_SOLD); + } catch (outboxError) { + console.error('Outbox ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ์‹คํŒจ:', outboxError); + // Outbox ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ์‹คํŒจ๋Š” ์ „์ฒด ํŠธ๋žœ์žญ์…˜์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Œ + } }); } catch (error) { console.error(error); @@ -475,13 +534,26 @@ export class StockService { Math.floor(getDateDistance(stock.startedTime, new Date()).minutes / stock.fluctuationsInterval), 9, ); - inventory.forEach((amount, company) => { - const companyPrice = companies.get(company)[idx]?.๊ฐ€๊ฒฉ; - const totalPrice = companyPrice * amount; - user.money += totalPrice; - remainingStocks.set(company, remainingStocks.get(company) + amount); - inventory.set(company, 0); + // ๊ฐ ํšŒ์‚ฌ๋ณ„ ์ฃผ์‹ ํŒ๋งค ์ฒ˜๋ฆฌ + const sellTransactions = []; + inventory.forEach((amount, company) => { + if (amount > 0) { + const companyPrice = companies.get(company)[idx]?.๊ฐ€๊ฒฉ; + const totalPrice = companyPrice * amount; + + user.money += totalPrice; + remainingStocks.set(company, remainingStocks.get(company) + amount); + inventory.set(company, 0); + + // ํŒ๋งค ํŠธ๋žœ์žญ์…˜ ์ •๋ณด ์ €์žฅ + sellTransactions.push({ + amount, + company, + totalPrice, + unitPrice: companyPrice, + }); + } }); const loanMoney = user.loanCount * StockConfig.SETTLE_LOAN_PRICE; @@ -489,6 +561,31 @@ export class StockService { user.loanCount = 0; await user.save({ session }); + + // ์‚ฌ์šฉ์ž๋ณ„๋กœ Outbox ํŒจํ„ด์„ ํ†ตํ•ด ๋ฉ”์‹œ์ง€ ์ ์žฌ (๋ชจ๋“  ํŒ๋งค ํŠธ๋žœ์žญ์…˜ ์ •๋ณด ํฌํ•จ) + if (sellTransactions.length > 0) { + try { + const payload = { + isAllSell: true, + stockId, + timestamp: new Date().toISOString(), + transactions: sellTransactions, + userId: user.userId, + }; + + await this.outboxService.createOutboxMessage( + OutboxEventType.STOCK_SOLD, + payload, + 'stock.transaction.topic', + { session }, + ); + + console.debug('Outbox ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ์„ฑ๊ณต:', OutboxEventType.STOCK_SOLD); + } catch (outboxError) { + console.error('Outbox ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ์‹คํŒจ:', outboxError); + // Outbox ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ์‹คํŒจ๋Š” ์ „์ฒด ํŠธ๋žœ์žญ์…˜์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Œ + } + } } result = await stock.save({ session }); }); @@ -563,7 +660,7 @@ export class StockService { async setStockPhase(stockId: string, phase: StockPhase): Promise { if (phase === 'INTRO_RESULT') { - await this.userService.alignIndexByOpenAI(stockId); + await this.userService.alignIndex(stockId); } return this.stockRepository.findOneAndUpdate(stockId, { $set: { stockPhase: phase } }); } diff --git a/package/feature/feature-nest-stock/test/integration/outbox.integration.spec.ts b/package/feature/feature-nest-stock/test/integration/outbox.integration.spec.ts new file mode 100644 index 00000000..f412a89c --- /dev/null +++ b/package/feature/feature-nest-stock/test/integration/outbox.integration.spec.ts @@ -0,0 +1,300 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MongooseModule, getModelToken } from '@nestjs/mongoose'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import { Connection, connect, Model } from 'mongoose'; +import { OutboxService } from '../../src/outbox/outbox.service'; +import { OutboxRepository } from '../../src/outbox/outbox.repository'; +import { OutboxProcessor } from '../../src/outbox/outbox.processor'; +import { KafkaService } from '../../src/kafka/kafka.service'; +import { Outbox, OutboxDocument, OutboxEventType, OutboxSchema, OutboxStatus } from '../../src/outbox/outbox.schema'; + +describe('Outbox Integration Tests', () => { + let moduleRef: TestingModule; + let mongod: MongoMemoryServer; + let mongoConnection: Connection; + let outboxModel: Model; + let outboxService: OutboxService; + + let outboxProcessor: OutboxProcessor; + let kafkaService: KafkaService; + let kafkaSendMessageMock: jest.Mock; + + beforeAll(async () => { + // ๋ฉ”๋ชจ๋ฆฌ MongoDB ์„œ๋ฒ„ ์‹œ์ž‘ + mongod = await MongoMemoryServer.create(); + const uri = mongod.getUri(); + mongoConnection = (await connect(uri)).connection; + + // Kafka ์„œ๋น„์Šค ๋ชจํ‚น ํ•จ์ˆ˜ ์ƒ์„ฑ + kafkaSendMessageMock = jest.fn().mockImplementation(() => Promise.resolve()); + + // ํ…Œ์ŠคํŠธ ๋ชจ๋“ˆ ์ƒ์„ฑ + moduleRef = await Test.createTestingModule({ + imports: [MongooseModule.forRoot(uri), MongooseModule.forFeature([{ name: Outbox.name, schema: OutboxSchema }])], + providers: [ + OutboxService, + OutboxRepository, + OutboxProcessor, + { + provide: KafkaService, + useValue: { + sendMessage: kafkaSendMessageMock, + }, + }, + ], + }).compile(); + + outboxModel = moduleRef.get>(getModelToken(Outbox.name)); + outboxService = moduleRef.get(OutboxService); + outboxProcessor = moduleRef.get(OutboxProcessor); + kafkaService = moduleRef.get(KafkaService); + }); + + afterAll(async () => { + await mongoConnection.dropDatabase(); + await mongoConnection.close(); + await mongod.stop(); + await moduleRef.close(); + }); + + beforeEach(async () => { + // ๊ฐ ํ…Œ์ŠคํŠธ ์ „์— ์ปฌ๋ ‰์…˜ ์ดˆ๊ธฐํ™” + await outboxModel.deleteMany({}); + // ๊ฐ ํ…Œ์ŠคํŠธ ์ „์— ๋ชจ๋“  ๋ชจ์˜ ํ•จ์ˆ˜ ์ดˆ๊ธฐํ™” + jest.clearAllMocks(); + // ๊ธฐ๋ณธ์ ์œผ๋กœ ์„ฑ๊ณต ์‘๋‹ต ์„ค์ • + kafkaSendMessageMock.mockImplementation(() => Promise.resolve()); + }); + + it('์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ๋ฐ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ', async () => { + // 1. ์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + const eventType = OutboxEventType.STOCK_PURCHASED; + const payload = { quantity: 10, stockId: '456', userId: '123' }; + const topic = 'stock.transaction.topic'; + + const outboxMessage = await outboxService.createOutboxMessage(eventType, payload, topic); + + expect(outboxMessage).toBeDefined(); + expect(outboxMessage.eventType).toBe(eventType); + expect(outboxMessage.status).toBe(OutboxStatus.PENDING); + expect(outboxMessage.topic).toBe(topic); + expect(JSON.parse(outboxMessage.payload)).toEqual(payload); + + // 2. ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ํ™•์ธ + await outboxProcessor.processOutboxMessages(); + + // 3. ๋ฉ”์‹œ์ง€ ์ƒํƒœ ํ™•์ธ + const processedMessage = await outboxModel.findById(outboxMessage._id); + expect(processedMessage).toBeDefined(); + expect(processedMessage.status).toBe(OutboxStatus.PROCESSED); + expect(processedMessage.processedAt).toBeDefined(); + + // 4. Kafka ๋ฉ”์‹œ์ง€ ์ „์†ก ํ™•์ธ + expect(kafkaService.sendMessage).toHaveBeenCalledWith(topic, outboxMessage.payload); + }); + + it('์‹คํŒจ ์ฒ˜๋ฆฌ ๋ฐ ๋ฉ”์‹œ์ง€ ์žฌ์‹œ๋„๊ฐ€ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ', async () => { + // 1. ์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + const eventType = OutboxEventType.STOCK_PURCHASED; + const payload = { quantity: 10, stockId: '456', userId: '123' }; + const topic = 'stock.transaction.topic'; + + const outboxMessage = await outboxService.createOutboxMessage(eventType, payload, topic); + + // 2. Kafka ์ „์†ก ์‹คํŒจ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (์ฒซ ๋ฒˆ์งธ ํ˜ธ์ถœ์—์„œ๋งŒ ์‹คํŒจ) + kafkaSendMessageMock.mockRejectedValueOnce(new Error('Kafka connection error')); + + // 3. ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์‹œ๋„ + await outboxProcessor.processOutboxMessages(); + + // 4. ์‹คํŒจ ์ƒํƒœ ํ™•์ธ + const failedMessage = await outboxModel.findById(outboxMessage._id); + expect(failedMessage).toBeDefined(); + expect(failedMessage.status).toBe(OutboxStatus.FAILED); + expect(failedMessage.errorMessage).toBe('Kafka connection error'); + expect(failedMessage.retryCount).toBe(1); + + // 5. Kafka ์ „์†ก ์„ฑ๊ณต์œผ๋กœ ๋ณต๊ตฌ (์ด๋ฏธ ๊ธฐ๋ณธ ์„ค์ •์ด ์„ฑ๊ณต์ด๋ฏ€๋กœ ์ถ”๊ฐ€ ์„ค์ • ๋ถˆํ•„์š”) + + // 6. ์žฌ์‹œ๋„ ์ฒ˜๋ฆฌ + await outboxProcessor.retryFailedMessages(); + + // 7. ์„ฑ๊ณต ์ƒํƒœ ํ™•์ธ + const retriedMessage = await outboxModel.findById(outboxMessage._id); + expect(retriedMessage).toBeDefined(); + expect(retriedMessage.status).toBe(OutboxStatus.PROCESSED); + expect(retriedMessage.processedAt).toBeDefined(); + }); + + it('์—ฌ๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ', async () => { + // 1. ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + const messages = [ + await outboxService.createOutboxMessage( + OutboxEventType.STOCK_PURCHASED, + { quantity: 10, stockId: '123', userId: '456' }, + 'stock.transaction.topic', + ), + await outboxService.createOutboxMessage( + OutboxEventType.STOCK_SOLD, + { quantity: 5, stockId: '789', userId: '456' }, + 'stock.transaction.topic', + ), + await outboxService.createOutboxMessage( + OutboxEventType.STOCK_INFO_DRAWN, + { name: 'Updated Stock', stockId: '123' }, + 'stock.info.topic', + ), + ]; + + // 2. ์•„์›ƒ๋ฐ•์Šค ํ”„๋กœ์„ธ์„œ ์‹คํ–‰ + await outboxProcessor.processOutboxMessages(); + + // 3. ๋ชจ๋“  ๋ฉ”์‹œ์ง€๊ฐ€ ์ฒ˜๋ฆฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + const processedMessages = await outboxModel.find({ status: OutboxStatus.PROCESSED }).sort({ createdAt: 1 }); + expect(processedMessages.length).toBe(3); + + // 4. ๋ฉ”์‹œ์ง€๊ฐ€ ์ˆœ์„œ๋Œ€๋กœ ์ฒ˜๋ฆฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + // ์ฐธ๊ณ : ID ๋น„๊ต ๋Œ€์‹  ํŽ˜์ด๋กœ๋“œ ๋‚ด์šฉ์œผ๋กœ ๋น„๊ต + expect(JSON.parse(processedMessages[0].payload)).toEqual(JSON.parse(messages[0].payload)); + expect(JSON.parse(processedMessages[1].payload)).toEqual(JSON.parse(messages[1].payload)); + expect(JSON.parse(processedMessages[2].payload)).toEqual(JSON.parse(messages[2].payload)); + + // 5. Kafka ๋ฉ”์‹œ์ง€ ์ „์†ก ํ™•์ธ + expect(kafkaService.sendMessage).toHaveBeenCalledTimes(3); + expect(kafkaService.sendMessage).toHaveBeenCalledWith('stock.transaction.topic', messages[0].payload); + expect(kafkaService.sendMessage).toHaveBeenCalledWith('stock.transaction.topic', messages[1].payload); + expect(kafkaService.sendMessage).toHaveBeenCalledWith('stock.info.topic', messages[2].payload); + }); + + it('๋™์ผ ๋ฉ”์‹œ์ง€์˜ ๋™์‹œ ์ฒ˜๋ฆฌ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•จ', async () => { + // 1. ์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + const eventType = OutboxEventType.STOCK_PURCHASED; + const payload = { quantity: 10, stockId: '456', userId: '123' }; + const topic = 'stock.transaction.topic'; + + const outboxMessage = await outboxService.createOutboxMessage(eventType, payload, topic); + + // 2. ๋™์‹œ ์ฒ˜๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์œ„ํ•œ ํ•จ์ˆ˜ ์ •์˜ + const processMessage: () => Promise = async () => { + try { + // ๋ฉ”์‹œ์ง€ ์กฐํšŒ + const messages = await outboxService.getPendingMessages(1); + if (messages.length > 0) { + // ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ (์ง€์—ฐ ์ถ”๊ฐ€) + await new Promise((resolve) => { + setTimeout(resolve, 10); + }); + await kafkaService.sendMessage(messages[0].topic, messages[0].payload); + // ์ƒํƒœ ์—…๋ฐ์ดํŠธ + await outboxService.updateOutboxStatus(messages[0]._id, OutboxStatus.PROCESSED); + } + } catch (error) { + console.error('์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + } + }; + + // 3. ๋™์‹œ์— ์—ฌ๋Ÿฌ ๋ฒˆ ๊ฐ™์€ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์‹œ๋„ + await Promise.all([processMessage(), processMessage(), processMessage()]); + + // 4. ๋ฉ”์‹œ์ง€ ์ƒํƒœ ํ™•์ธ (ํ•œ ๋ฒˆ๋งŒ ์ฒ˜๋ฆฌ๋˜์–ด์•ผ ํ•จ) + const processedMessage = await outboxModel.findById(outboxMessage._id); + expect(processedMessage).toBeDefined(); + expect(processedMessage.status).toBe(OutboxStatus.PROCESSED); + + // 5. Kafka ๋ฉ”์‹œ์ง€ ์ „์†ก ํ™•์ธ (ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœ๋˜์–ด์•ผ ํ•จ) + // ์ฐธ๊ณ : ์‹ค์ œ๋กœ๋Š” ์—ฌ๋Ÿฌ ๋ฒˆ ํ˜ธ์ถœ๋  ์ˆ˜ ์žˆ์ง€๋งŒ, ๋ฉฑ๋“ฑ์„ฑ์ด ๋ณด์žฅ๋˜์–ด์•ผ ํ•จ + expect(kafkaService.sendMessage).toHaveBeenCalledWith(topic, outboxMessage.payload); + }); + + it('ํŠธ๋žœ์žญ์…˜ ๋ฐ๋“œ๋ฝ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ', async () => { + // MongoDB ๋ฉ”๋ชจ๋ฆฌ ์„œ๋ฒ„์—์„œ๋Š” ํŠธ๋žœ์žญ์…˜ ๊ด€๋ จ ํ…Œ์ŠคํŠธ๊ฐ€ ์ œํ•œ์ ์ด๋ฏ€๋กœ + // ๊ฐ„์†Œํ™”๋œ ์‹œ๋‚˜๋ฆฌ์˜ค๋กœ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค + + // 1. ๋‘ ๊ฐœ์˜ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + const message1 = await outboxService.createOutboxMessage( + OutboxEventType.STOCK_PURCHASED, + { quantity: 10, stockId: '456', userId: '123' }, + 'stock.transaction.topic', + ); + + const message2 = await outboxService.createOutboxMessage( + OutboxEventType.STOCK_SOLD, + { quantity: 5, stockId: '789', userId: '123' }, + 'stock.transaction.topic', + ); + + // 2. ๋™์‹œ ์—…๋ฐ์ดํŠธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์œ„ํ•œ ํ•จ์ˆ˜ ์ •์˜ + const updateMessage: (messageId: string, delay: number) => Promise = async ( + messageId: string, + delay: number, + ) => { + await new Promise((resolve) => { + setTimeout(resolve, delay); + }); + try { + return await outboxService.updateOutboxStatus(messageId, OutboxStatus.PROCESSED); + } catch (error) { + console.error('์—…๋ฐ์ดํŠธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ:', error); + return null; + } + }; + + // 3. ๋™์‹œ์— ๋‘ ๋ฉ”์‹œ์ง€ ์—…๋ฐ์ดํŠธ ์‹œ๋„ (์„œ๋กœ ๋‹ค๋ฅธ ์ง€์—ฐ์œผ๋กœ) + const results = await Promise.all([updateMessage(message1._id, 0), updateMessage(message2._id, 5)]); + + // 4. ๊ฒฐ๊ณผ ํ™•์ธ (๋‘ ์—…๋ฐ์ดํŠธ ๋ชจ๋‘ ์„ฑ๊ณตํ•ด์•ผ ํ•จ) + expect(results[0]).not.toBeNull(); + expect(results[1]).not.toBeNull(); + + // 5. ๋ฉ”์‹œ์ง€ ์ƒํƒœ ํ™•์ธ + const finalMessages = await outboxModel.find({ + _id: { $in: [message1._id, message2._id] }, + }); + + expect(finalMessages.length).toBe(2); + expect(finalMessages[0].status).toBe(OutboxStatus.PROCESSED); + expect(finalMessages[1].status).toBe(OutboxStatus.PROCESSED); + }); + + it('๋‚™๊ด€์  ๋™์‹œ์„ฑ ์ œ์–ด๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•จ', async () => { + // 1. ์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + const eventType = OutboxEventType.STOCK_PURCHASED; + const payload = { quantity: 10, stockId: '456', userId: '123' }; + const topic = 'stock.transaction.topic'; + + const outboxMessage = await outboxService.createOutboxMessage(eventType, payload, topic); + + // 2. ๋™์‹œ ์—…๋ฐ์ดํŠธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์œ„ํ•œ ํ•จ์ˆ˜ ์ •์˜ + const updateMessage: (status: OutboxStatus, delay: number) => Promise = async ( + status: OutboxStatus, + delay: number, + ) => { + await new Promise((resolve) => { + setTimeout(resolve, delay); + }); + try { + return await outboxService.updateOutboxStatus(outboxMessage._id, status); + } catch (error) { + return null; + } + }; + + // 3. ๋™์‹œ์— ์—ฌ๋Ÿฌ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์‹œ๋„ (์„œ๋กœ ๋‹ค๋ฅธ ์ง€์—ฐ์œผ๋กœ) + const results = await Promise.all([ + updateMessage(OutboxStatus.PROCESSED, 0), + updateMessage(OutboxStatus.FAILED, 5), + updateMessage(OutboxStatus.PROCESSED, 10), + ]); + + // 4. ์ตœ์ข… ๋ฉ”์‹œ์ง€ ์ƒํƒœ ํ™•์ธ + const finalMessage = await outboxModel.findById(outboxMessage._id); + expect(finalMessage).toBeDefined(); + + // 5. ์„ฑ๊ณตํ•œ ์—…๋ฐ์ดํŠธ ์ˆ˜ ํ™•์ธ (๋ชจ๋“  ์—…๋ฐ์ดํŠธ๊ฐ€ ์„ฑ๊ณตํ–ˆ์„ ์ˆ˜ ์žˆ์Œ) + const successCount = results.filter((r) => r !== null).length; + expect(successCount).toBeGreaterThan(0); + + // 6. ๋งˆ์ง€๋ง‰ ์ƒํƒœ๋Š” ์—…๋ฐ์ดํŠธ ์ค‘ ํ•˜๋‚˜์™€ ์ผ์น˜ํ•ด์•ผ ํ•จ + expect([OutboxStatus.PROCESSED, OutboxStatus.FAILED]).toContain(finalMessage.status); + }); +}); diff --git a/package/feature/feature-nest-stock/test/jest-e2e.json b/package/feature/feature-nest-stock/test/jest-e2e.json new file mode 100644 index 00000000..e9d912f3 --- /dev/null +++ b/package/feature/feature-nest-stock/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/package/feature/feature-nest-stock/test/unit/outbox/outbox.processor.spec.ts b/package/feature/feature-nest-stock/test/unit/outbox/outbox.processor.spec.ts new file mode 100644 index 00000000..cedc8521 --- /dev/null +++ b/package/feature/feature-nest-stock/test/unit/outbox/outbox.processor.spec.ts @@ -0,0 +1,179 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { Logger } from '@nestjs/common'; +import { OutboxProcessor } from '../../../src/outbox/outbox.processor'; +import { OutboxService } from '../../../src/outbox/outbox.service'; +import { KafkaService } from '../../../src/kafka/kafka.service'; +import { OutboxDocument, OutboxEventType, OutboxStatus } from '../../../src/outbox/outbox.schema'; + +// ๋ชฉ ํƒ€์ž… ์ •์˜ +type MockType = { + [P in keyof T]?: jest.Mock; +}; + +// ๋ชฉ ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜ +const mockOutboxService = (): MockType => ({ + getFailedMessages: jest.fn(), + getPendingMessages: jest.fn(), + updateOutboxStatus: jest.fn(), +}); + +const mockKafkaService = (): MockType => ({ + sendMessage: jest.fn(), +}); + +describe('OutboxProcessor', () => { + let outboxProcessor: OutboxProcessor; + let outboxService: MockType; + let kafkaService: MockType; + + beforeEach(async () => { + // Logger ๋ชจํ‚น ์„ค์ • + jest.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined); + jest.spyOn(Logger.prototype, 'debug').mockImplementation(() => undefined); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + OutboxProcessor, + { + provide: OutboxService, + useFactory: mockOutboxService, + }, + { + provide: KafkaService, + useFactory: mockKafkaService, + }, + ], + }).compile(); + + outboxProcessor = module.get(OutboxProcessor); + outboxService = module.get(OutboxService); + kafkaService = module.get(KafkaService); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('ํ”„๋กœ์„ธ์„œ๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์–ด์•ผ ํ•จ', () => { + expect(outboxProcessor).toBeDefined(); + }); + + describe('processOutboxMessages', () => { + it('๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•จ', async () => { + // Given + const mockPendingMessages = [ + { + _id: '1', + eventType: OutboxEventType.STOCK_PURCHASED, + payload: JSON.stringify({ stockId: '456', userId: '123' }), + retryCount: 0, + status: OutboxStatus.PENDING, + topic: 'stock.topic', + }, + ] as unknown as OutboxDocument[]; + + outboxService.getPendingMessages.mockReturnValue(Promise.resolve(mockPendingMessages)); + kafkaService.sendMessage.mockReturnValue(Promise.resolve()); + outboxService.updateOutboxStatus.mockReturnValue(Promise.resolve({} as OutboxDocument)); + + // When + await outboxProcessor.processOutboxMessages(); + + // Then + expect(outboxService.getPendingMessages).toHaveBeenCalledWith(10); + expect(kafkaService.sendMessage).toHaveBeenCalledWith( + mockPendingMessages[0].topic, + mockPendingMessages[0].payload, + ); + expect(outboxService.updateOutboxStatus).toHaveBeenCalledWith(mockPendingMessages[0]._id, OutboxStatus.PROCESSED); + }); + + it('๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์‹œ ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•จ', async () => { + // Given + const mockPendingMessages = [ + { + _id: '1', + eventType: OutboxEventType.STOCK_PURCHASED, + payload: JSON.stringify({ stockId: '456', userId: '123' }), + retryCount: 0, + status: OutboxStatus.PENDING, + topic: 'stock.topic', + }, + ] as unknown as OutboxDocument[]; + + const error = new Error('Kafka connection error'); + outboxService.getPendingMessages.mockReturnValue(Promise.resolve(mockPendingMessages)); + kafkaService.sendMessage.mockRejectedValue(error); + outboxService.updateOutboxStatus.mockReturnValue(Promise.resolve({} as OutboxDocument)); + + // When + await outboxProcessor.processOutboxMessages(); + + // Then + expect(outboxService.updateOutboxStatus).toHaveBeenCalledWith( + mockPendingMessages[0]._id, + OutboxStatus.FAILED, + error.message, + ); + expect(Logger.prototype.error).toHaveBeenCalled(); + }); + }); + + describe('retryFailedMessages', () => { + it('์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ์žฌ์‹œ๋„ํ•ด์•ผ ํ•จ', async () => { + // Given + const mockFailedMessages = [ + { + _id: '1', + eventType: OutboxEventType.STOCK_PURCHASED, + payload: JSON.stringify({ stockId: '456', userId: '123' }), + retryCount: 1, + status: OutboxStatus.FAILED, + topic: 'stock.topic', + }, + ] as unknown as OutboxDocument[]; + + outboxService.getFailedMessages.mockReturnValue(Promise.resolve(mockFailedMessages)); + kafkaService.sendMessage.mockReturnValue(Promise.resolve()); + outboxService.updateOutboxStatus.mockReturnValue(Promise.resolve({} as OutboxDocument)); + + // When + await outboxProcessor.retryFailedMessages(); + + // Then + expect(outboxService.getFailedMessages).toHaveBeenCalledWith(3); + expect(kafkaService.sendMessage).toHaveBeenCalledWith(mockFailedMessages[0].topic, mockFailedMessages[0].payload); + expect(outboxService.updateOutboxStatus).toHaveBeenCalledWith(mockFailedMessages[0]._id, OutboxStatus.PROCESSED); + }); + + it('์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€ ์žฌ์‹œ๋„ ์‹œ ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•จ', async () => { + // Given + const mockFailedMessages = [ + { + _id: '1', + eventType: OutboxEventType.STOCK_PURCHASED, + payload: JSON.stringify({ stockId: '456', userId: '123' }), + retryCount: 1, + status: OutboxStatus.FAILED, + topic: 'stock.topic', + }, + ] as unknown as OutboxDocument[]; + + const error = new Error('Kafka connection error'); + outboxService.getFailedMessages.mockReturnValue(Promise.resolve(mockFailedMessages)); + kafkaService.sendMessage.mockRejectedValue(error); + outboxService.updateOutboxStatus.mockReturnValue(Promise.resolve({} as OutboxDocument)); + + // When + await outboxProcessor.retryFailedMessages(); + + // Then + expect(outboxService.updateOutboxStatus).toHaveBeenCalledWith( + mockFailedMessages[0]._id, + OutboxStatus.FAILED, + error.message, + ); + expect(Logger.prototype.error).toHaveBeenCalled(); + }); + }); +}); diff --git a/package/feature/feature-nest-stock/test/unit/outbox/outbox.service.spec.ts b/package/feature/feature-nest-stock/test/unit/outbox/outbox.service.spec.ts new file mode 100644 index 00000000..4cef994d --- /dev/null +++ b/package/feature/feature-nest-stock/test/unit/outbox/outbox.service.spec.ts @@ -0,0 +1,193 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { Logger } from '@nestjs/common'; +import { getConnectionToken } from '@nestjs/mongoose'; +import { Connection } from 'mongoose'; +import { OutboxService } from '../../../src/outbox/outbox.service'; +import { OutboxRepository } from '../../../src/outbox/outbox.repository'; +import { OutboxDocument, OutboxEventType, OutboxStatus } from '../../../src/outbox/outbox.schema'; + +// ๋ชฉ ํƒ€์ž… ์ •์˜ +type MockType = { + [P in keyof T]?: jest.Mock; +}; + +// ๋ชฉ ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜ +const mockRepository = (): MockType => ({ + create: jest.fn(), + findFailedEvents: jest.fn(), + findPendingEvents: jest.fn(), + markAsFailed: jest.fn(), + markAsProcessed: jest.fn(), +}); + +const mockConnection = (): MockType => ({ + startSession: jest.fn(), +}); + +describe('OutboxService', () => { + let outboxService: OutboxService; + let outboxRepository: MockType; + + beforeEach(async () => { + // Logger ๋ชจํ‚น ์„ค์ • + jest.spyOn(Logger.prototype, 'error').mockImplementation(() => undefined); + jest.spyOn(Logger.prototype, 'debug').mockImplementation(() => undefined); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + OutboxService, + { + provide: OutboxRepository, + useFactory: mockRepository, + }, + { + provide: getConnectionToken(), + useFactory: mockConnection, + }, + ], + }).compile(); + + outboxService = module.get(OutboxService); + outboxRepository = module.get(OutboxRepository); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('์„œ๋น„์Šค๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์–ด์•ผ ํ•จ', () => { + expect(outboxService).toBeDefined(); + }); + + describe('createOutboxMessage', () => { + it('์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•จ', async () => { + // Given + const eventType: OutboxEventType = OutboxEventType.STOCK_PURCHASED; + const payload = { quantity: 10, stockId: '456', userId: '123' }; + const topic = 'stock.transaction.topic'; + const mockOutboxDoc = { + _id: 'abc123', + eventType, + payload: JSON.stringify(payload), + retryCount: 0, + status: OutboxStatus.PENDING, + topic, + } as unknown as OutboxDocument; + + outboxRepository.create.mockReturnValue(Promise.resolve(mockOutboxDoc)); + + // When + const result = await outboxService.createOutboxMessage(eventType, payload, topic); + + // Then + expect(outboxRepository.create).toHaveBeenCalledWith(eventType, payload, topic, undefined); + expect(result).toEqual(mockOutboxDoc); + }); + + it('์•„์›ƒ๋ฐ•์Šค ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ ์‹œ ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•จ', async () => { + // Given + const error = new Error('Database connection error'); + outboxRepository.create.mockRejectedValue(error); + + // When/Then + await expect(outboxService.createOutboxMessage(OutboxEventType.STOCK_PURCHASED, {}, 'topic')).rejects.toThrow( + error, + ); + + expect(Logger.prototype.error).toHaveBeenCalled(); + }); + }); + + describe('getPendingMessages', () => { + it('๋Œ€๊ธฐ ์ค‘์ธ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•จ', async () => { + // Given + const mockPendingMessages = [ + { _id: '1', retryCount: 0, status: OutboxStatus.PENDING }, + { _id: '2', retryCount: 0, status: OutboxStatus.PENDING }, + ] as unknown as OutboxDocument[]; + + outboxRepository.findPendingEvents.mockReturnValue(Promise.resolve(mockPendingMessages)); + + // When + const result = await outboxService.getPendingMessages(5); + + // Then + expect(outboxRepository.findPendingEvents).toHaveBeenCalledWith(5); + expect(result).toEqual(mockPendingMessages); + }); + }); + + describe('updateOutboxStatus', () => { + it('์•„์›ƒ๋ฐ•์Šค ์ƒํƒœ๋ฅผ ์ฒ˜๋ฆฌ๋จ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•จ', async () => { + // Given + const messageId = 'abc123'; + const status: OutboxStatus = OutboxStatus.PROCESSED; + const mockUpdatedDoc = { + _id: messageId, + processedAt: expect.any(Date), + status, + } as unknown as OutboxDocument; + + outboxRepository.markAsProcessed.mockReturnValue(Promise.resolve(mockUpdatedDoc)); + + // When + const result = await outboxService.updateOutboxStatus(messageId, status); + + // Then + expect(outboxRepository.markAsProcessed).toHaveBeenCalledWith(messageId); + expect(result).toEqual(mockUpdatedDoc); + }); + + it('์•„์›ƒ๋ฐ•์Šค ์ƒํƒœ๋ฅผ ์‹คํŒจ๋กœ ์—…๋ฐ์ดํŠธํ•ด์•ผ ํ•จ', async () => { + // Given + const messageId = 'abc123'; + const status: OutboxStatus = OutboxStatus.FAILED; + const errorMessage = 'Kafka connection error'; + const mockUpdatedDoc = { + _id: messageId, + errorMessage, + retryCount: 1, + status, + } as unknown as OutboxDocument; + + outboxRepository.markAsFailed.mockReturnValue(Promise.resolve(mockUpdatedDoc)); + + // When + const result = await outboxService.updateOutboxStatus(messageId, status, errorMessage); + + // Then + expect(outboxRepository.markAsFailed).toHaveBeenCalledWith(messageId, errorMessage); + expect(result).toEqual(mockUpdatedDoc); + }); + + it('์•„์›ƒ๋ฐ•์Šค ์ƒํƒœ ์—…๋ฐ์ดํŠธ ์‹œ ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•จ', async () => { + // Given + const error = new Error('Database connection error'); + outboxRepository.markAsProcessed.mockRejectedValue(error); + + // When/Then + await expect(outboxService.updateOutboxStatus('abc123', OutboxStatus.PROCESSED)).rejects.toThrow(error); + + expect(Logger.prototype.error).toHaveBeenCalled(); + }); + }); + + describe('getFailedMessages', () => { + it('์‹คํŒจํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•จ', async () => { + // Given + const mockFailedMessages = [ + { _id: '1', retryCount: 1, status: OutboxStatus.FAILED }, + { _id: '2', retryCount: 2, status: OutboxStatus.FAILED }, + ] as unknown as OutboxDocument[]; + + outboxRepository.findFailedEvents.mockReturnValue(Promise.resolve(mockFailedMessages)); + + // When + const result = await outboxService.getFailedMessages(3); + + // Then + expect(outboxRepository.findFailedEvents).toHaveBeenCalledWith(3); + expect(result).toEqual(mockFailedMessages); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index f9128c9f..aeaf3eb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -947,6 +947,18 @@ __metadata: languageName: node linkType: hard +"@confluentinc/kafka-javascript@npm:^1.2.0": + version: 1.2.0 + resolution: "@confluentinc/kafka-javascript@npm:1.2.0" + dependencies: + "@mapbox/node-pre-gyp": "npm:^1.0.11" + bindings: "npm:^1.3.1" + nan: "npm:^2.22.0" + node-gyp: "npm:latest" + checksum: 32d1a18e8d7bf838bf82fdb8bc298a02a365e4388cb47e8258887ff43ab39979fad4ffd608c141b7709012e54d46b59bfe023b02e7ecca28a80e75687b5d04d4 + languageName: node + linkType: hard + "@cspotcode/source-map-support@npm:^0.8.0": version: 0.8.1 resolution: "@cspotcode/source-map-support@npm:0.8.1" @@ -2186,6 +2198,25 @@ __metadata: languageName: node linkType: hard +"@mapbox/node-pre-gyp@npm:^1.0.11": + version: 1.0.11 + resolution: "@mapbox/node-pre-gyp@npm:1.0.11" + dependencies: + detect-libc: "npm:^2.0.0" + https-proxy-agent: "npm:^5.0.0" + make-dir: "npm:^3.1.0" + node-fetch: "npm:^2.6.7" + nopt: "npm:^5.0.0" + npmlog: "npm:^5.0.1" + rimraf: "npm:^3.0.2" + semver: "npm:^7.3.5" + tar: "npm:^6.1.11" + bin: + node-pre-gyp: bin/node-pre-gyp + checksum: 59529a2444e44fddb63057152452b00705aa58059079191126c79ac1388ae4565625afa84ed4dd1bf017d1111ab6e47907f7c5192e06d83c9496f2f3e708680a + languageName: node + linkType: hard + "@mongodb-js/saslprep@npm:^1.1.0": version: 1.1.0 resolution: "@mongodb-js/saslprep@npm:1.1.0" @@ -2195,6 +2226,15 @@ __metadata: languageName: node linkType: hard +"@mongodb-js/saslprep@npm:^1.1.9": + version: 1.2.0 + resolution: "@mongodb-js/saslprep@npm:1.2.0" + dependencies: + sparse-bitfield: "npm:^3.0.3" + checksum: 2c50080844d2738f277a286a4d2c96422da0f2d8a377bb5723eb3b1128fb3eddb67f6e7392fc03ef771dad152cc109f5fa8e7c1d6c0830e17759fd616fd8d917 + languageName: node + linkType: hard + "@nestjs/cli@npm:^10.3.0": version: 10.3.0 resolution: "@nestjs/cli@npm:10.3.0" @@ -2299,6 +2339,49 @@ __metadata: languageName: node linkType: hard +"@nestjs/microservices@npm:^11.0.7": + version: 11.0.7 + resolution: "@nestjs/microservices@npm:11.0.7" + dependencies: + iterare: "npm:1.2.1" + tslib: "npm:2.8.1" + peerDependencies: + "@grpc/grpc-js": "*" + "@nestjs/common": ^11.0.0 + "@nestjs/core": ^11.0.0 + "@nestjs/websockets": ^11.0.0 + amqp-connection-manager: "*" + amqplib: "*" + cache-manager: "*" + ioredis: "*" + kafkajs: "*" + mqtt: "*" + nats: "*" + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + "@grpc/grpc-js": + optional: true + "@nestjs/websockets": + optional: true + amqp-connection-manager: + optional: true + amqplib: + optional: true + cache-manager: + optional: true + ioredis: + optional: true + kafkajs: + optional: true + mqtt: + optional: true + nats: + optional: true + checksum: a7147f2ae04aa34abd8af0e617d6af31b6a006ea50ba9ddf4dfa2abfa0e610a264974b130d78e745741ab5e4b88e5b7bc93b909f350a180369848415975c322a + languageName: node + linkType: hard + "@nestjs/mongoose@npm:^10.0.2": version: 10.0.2 resolution: "@nestjs/mongoose@npm:10.0.2" @@ -2342,6 +2425,18 @@ __metadata: languageName: node linkType: hard +"@nestjs/schedule@npm:^5.0.1": + version: 5.0.1 + resolution: "@nestjs/schedule@npm:5.0.1" + dependencies: + cron: "npm:3.5.0" + peerDependencies: + "@nestjs/common": ^10.0.0 || ^11.0.0 + "@nestjs/core": ^10.0.0 || ^11.0.0 + checksum: fe14ce3bd010cc00f7123106854f58d2c63f5578794ec36b29389c91b6f257963d354ba8fb5d99907875835c1b5c3c2a746c8f73938b2e7b13bc89f0bcb6c057 + languageName: node + linkType: hard + "@nestjs/schematics@npm:^10.0.1, @nestjs/schematics@npm:^10.0.2": version: 10.0.2 resolution: "@nestjs/schematics@npm:10.0.2" @@ -3553,6 +3648,13 @@ __metadata: languageName: node linkType: hard +"@types/luxon@npm:~3.4.0": + version: 3.4.2 + resolution: "@types/luxon@npm:3.4.2" + checksum: fd89566e3026559f2bc4ddcc1e70a2c16161905ed50be9473ec0cfbbbe919165041408c4f6e06c4bcf095445535052e2c099087c76b1b38e368127e618fc968d + languageName: node + linkType: hard + "@types/mime@npm:*": version: 3.0.1 resolution: "@types/mime@npm:3.0.1" @@ -4404,7 +4506,7 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:^1.0.0": +"abbrev@npm:1, abbrev@npm:^1.0.0": version: 1.1.1 resolution: "abbrev@npm:1.1.1" checksum: 2d882941183c66aa665118bafdab82b7a177e9add5eb2776c33e960a4f3c89cff88a1b38aba13a456de01d0dd9d66a8bea7c903268b21ea91dd1097e1e2e8243 @@ -4491,6 +4593,13 @@ __metadata: languageName: node linkType: hard +"agent-base@npm:^7.1.2": + version: 7.1.3 + resolution: "agent-base@npm:7.1.3" + checksum: 3db6d8d4651f2aa1a9e4af35b96ab11a7607af57a24f3bc721a387eaa3b5f674e901f0a648b0caefd48f3fd117c7761b79a3b55854e2aebaa96c3f32cf76af84 + languageName: node + linkType: hard + "agentkeepalive@npm:^4.2.1": version: 4.3.0 resolution: "agentkeepalive@npm:4.3.0" @@ -4704,6 +4813,16 @@ __metadata: languageName: node linkType: hard +"are-we-there-yet@npm:^2.0.0": + version: 2.0.0 + resolution: "are-we-there-yet@npm:2.0.0" + dependencies: + delegates: "npm:^1.0.0" + readable-stream: "npm:^3.6.0" + checksum: ea6f47d14fc33ae9cbea3e686eeca021d9d7b9db83a306010dd04ad5f2c8b7675291b127d3fcbfcbd8fec26e47b3324ad5b469a6cc3733a582f2fe4e12fc6756 + languageName: node + linkType: hard + "are-we-there-yet@npm:^3.0.0": version: 3.0.1 resolution: "are-we-there-yet@npm:3.0.1" @@ -4848,6 +4967,15 @@ __metadata: languageName: node linkType: hard +"async-mutex@npm:^0.5.0": + version: 0.5.0 + resolution: "async-mutex@npm:0.5.0" + dependencies: + tslib: "npm:^2.4.0" + checksum: 4c6bfce1cc9cd43f723c4d96403ac5f4757f885c953b839cde6956ec8817ff39623b82d67614de10c7933e21626925882fb9bac367db7d15d7cb4f84228722c9 + languageName: node + linkType: hard + "async-validator@npm:^4.1.0": version: 4.2.5 resolution: "async-validator@npm:4.2.5" @@ -4917,6 +5045,13 @@ __metadata: languageName: node linkType: hard +"b4a@npm:^1.6.4": + version: 1.6.7 + resolution: "b4a@npm:1.6.7" + checksum: 1ac056e3bce378d4d3e570e57319360a9d3125ab6916a1921b95bea33d9ee646698ebc75467561fd6fcc80ff697612124c89bb9b95e80db94c6dc23fcb977705 + languageName: node + linkType: hard + "babel-jest@npm:^29.5.0": version: 29.5.0 resolution: "babel-jest@npm:29.5.0" @@ -5023,6 +5158,13 @@ __metadata: languageName: node linkType: hard +"bare-events@npm:^2.2.0": + version: 2.5.4 + resolution: "bare-events@npm:2.5.4" + checksum: 135ef380b13f554ca2c6905bdbcfac8edae08fce85b7f953fa01f09a9f5b0da6a25e414111659bc9a6118216f0dd1f732016acd11ce91517f2afb26ebeb4b721 + languageName: node + linkType: hard + "base64-arraybuffer@npm:^1.0.2": version: 1.0.2 resolution: "base64-arraybuffer@npm:1.0.2" @@ -5058,6 +5200,15 @@ __metadata: languageName: node linkType: hard +"bindings@npm:^1.3.1": + version: 1.5.0 + resolution: "bindings@npm:1.5.0" + dependencies: + file-uri-to-path: "npm:1.0.0" + checksum: 593d5ae975ffba15fbbb4788fe5abd1e125afbab849ab967ab43691d27d6483751805d98cb92f7ac24a2439a8a8678cd0131c535d5d63de84e383b0ce2786133 + languageName: node + linkType: hard + "bl@npm:^4.1.0": version: 4.1.0 resolution: "bl@npm:4.1.0" @@ -5201,6 +5352,13 @@ __metadata: languageName: node linkType: hard +"bson@npm:^6.10.3": + version: 6.10.3 + resolution: "bson@npm:6.10.3" + checksum: 53693ad7636fa1a128f32d5c30a9584b4a09a2b2aa2040ea8bf8dca35a283eb1394138770bc6c2dae91b0c6df96b3caef3b14532644736f4d7a1d3177969537b + languageName: node + linkType: hard + "bson@npm:^6.2.0": version: 6.2.0 resolution: "bson@npm:6.2.0" @@ -5208,6 +5366,13 @@ __metadata: languageName: node linkType: hard +"buffer-crc32@npm:~0.2.3": + version: 0.2.13 + resolution: "buffer-crc32@npm:0.2.13" + checksum: 06252347ae6daca3453b94e4b2f1d3754a3b146a111d81c68924c22d91889a40623264e95e67955b1cb4a68cbedf317abeabb5140a9766ed248973096db5ce1c + languageName: node + linkType: hard + "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -5312,7 +5477,7 @@ __metadata: languageName: node linkType: hard -"camelcase@npm:^6.2.0": +"camelcase@npm:^6.2.0, camelcase@npm:^6.3.0": version: 6.3.0 resolution: "camelcase@npm:6.3.0" checksum: 8c96818a9076434998511251dcb2761a94817ea17dbdc37f47ac080bd088fc62c7369429a19e2178b993497132c8cbcf5cc1f44ba963e76782ba469c0474938d @@ -5557,7 +5722,7 @@ __metadata: languageName: node linkType: hard -"color-support@npm:^1.1.3": +"color-support@npm:^1.1.2, color-support@npm:^1.1.3": version: 1.1.3 resolution: "color-support@npm:1.1.3" bin: @@ -5616,6 +5781,13 @@ __metadata: languageName: node linkType: hard +"commondir@npm:^1.0.1": + version: 1.0.1 + resolution: "commondir@npm:1.0.1" + checksum: 4620bc4936a4ef12ce7dfcd272bb23a99f2ad68889a4e4ad766c9f8ad21af982511934d6f7050d4a8bde90011b1c15d56e61a1b4576d9913efbf697a20172d6c + languageName: node + linkType: hard + "component-emitter@npm:^1.3.0": version: 1.3.0 resolution: "component-emitter@npm:1.3.0" @@ -5663,7 +5835,7 @@ __metadata: languageName: node linkType: hard -"console-control-strings@npm:^1.1.0": +"console-control-strings@npm:^1.0.0, console-control-strings@npm:^1.1.0": version: 1.1.0 resolution: "console-control-strings@npm:1.1.0" checksum: 27b5fa302bc8e9ae9e98c03c66d76ca289ad0c61ce2fe20ab288d288bee875d217512d2edb2363fc83165e88f1c405180cf3f5413a46e51b4fe1a004840c6cdb @@ -5800,6 +5972,16 @@ __metadata: languageName: node linkType: hard +"cron@npm:3.5.0": + version: 3.5.0 + resolution: "cron@npm:3.5.0" + dependencies: + "@types/luxon": "npm:~3.4.0" + luxon: "npm:~3.5.0" + checksum: 0e667d87c9acc162db835439bff2664483f1fcbd471ae30a26c7426c736fa1798d27067cc4d0294d8a27890a1bc6c9deeefe47811cc339f11a8ba8288f51886d + languageName: node + linkType: hard + "cross-env@npm:^7.0.3": version: 7.0.3 resolution: "cross-env@npm:7.0.3" @@ -5924,6 +6106,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.3.7": + version: 4.4.0 + resolution: "debug@npm:4.4.0" + dependencies: + ms: "npm:^2.1.3" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 1847944c2e3c2c732514b93d11886575625686056cd765336212dc15de2d2b29612b6cd80e1afba767bb8e1803b778caf9973e98169ef1a24a7a7009e1820367 + languageName: node + linkType: hard + "dedent@npm:^0.7.0": version: 0.7.0 resolution: "dedent@npm:0.7.0" @@ -6065,6 +6259,13 @@ __metadata: languageName: node linkType: hard +"detect-libc@npm:^2.0.0": + version: 2.0.3 + resolution: "detect-libc@npm:2.0.3" + checksum: b4ea018d623e077bd395f168a9e81db77370dde36a5b01d067f2ad7989924a81d31cb547ff764acb2aa25d50bb7fdde0b0a93bec02212b0cb430621623246d39 + languageName: node + linkType: hard + "detect-newline@npm:^3.0.0": version: 3.1.0 resolution: "detect-newline@npm:3.1.0" @@ -7271,6 +7472,13 @@ __metadata: languageName: node linkType: hard +"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": + version: 1.3.2 + resolution: "fast-fifo@npm:1.3.2" + checksum: 6bfcba3e4df5af7be3332703b69a7898a8ed7020837ec4395bb341bd96cc3a6d86c3f6071dd98da289618cf2234c70d84b2a6f09a33dd6f988b1ff60d8e54275 + languageName: node + linkType: hard + "fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.9": version: 3.2.12 resolution: "fast-glob@npm:3.2.12" @@ -7396,6 +7604,7 @@ __metadata: "@nestjs/core": "npm:^10.3.0" "@nestjs/mongoose": "npm:^10.0.2" "@nestjs/platform-express": "npm:^10.3.0" + "@nestjs/schedule": "npm:^5.0.1" "@nestjs/schematics": "npm:^10.0.2" "@nestjs/testing": "npm:^10.3.0" "@toss/date": "npm:^1.1.7" @@ -7423,8 +7632,10 @@ __metadata: eslint-plugin-sort-keys-fix: "npm:^1.1.2" eslint-plugin-unused-imports: "npm:^2.0.0" jest: "npm:29.5.0" + kafkajs: "npm:^2.2.4" lodash: "npm:^4.17.21" mongodb: "npm:6.3.0" + mongodb-memory-server: "npm:^10.1.4" mongoose: "npm:^8.1.0" openai: "npm:^4.85.1" prettier: "npm:^2.6.1" @@ -7487,6 +7698,13 @@ __metadata: languageName: node linkType: hard +"file-uri-to-path@npm:1.0.0": + version: 1.0.0 + resolution: "file-uri-to-path@npm:1.0.0" + checksum: b648580bdd893a008c92c7ecc96c3ee57a5e7b6c4c18a9a09b44fb5d36d79146f8e442578bc0e173dc027adf3987e254ba1dfd6e3ec998b7c282873010502144 + languageName: node + linkType: hard + "fill-range@npm:^7.0.1": version: 7.0.1 resolution: "fill-range@npm:7.0.1" @@ -7520,6 +7738,17 @@ __metadata: languageName: node linkType: hard +"find-cache-dir@npm:^3.3.2": + version: 3.3.2 + resolution: "find-cache-dir@npm:3.3.2" + dependencies: + commondir: "npm:^1.0.1" + make-dir: "npm:^3.0.2" + pkg-dir: "npm:^4.1.0" + checksum: 3907c2e0b15132704ed67083686cd3e68ab7d9ecc22e50ae9da20678245d488b01fa22c0e34c0544dc6edc4354c766f016c8c186a787be7c17f7cde8c5281e85 + languageName: node + linkType: hard + "find-root@npm:^1.1.0": version: 1.1.0 resolution: "find-root@npm:1.1.0" @@ -7581,6 +7810,16 @@ __metadata: languageName: node linkType: hard +"follow-redirects@npm:^1.15.9": + version: 1.15.9 + resolution: "follow-redirects@npm:1.15.9" + peerDependenciesMeta: + debug: + optional: true + checksum: e3ab42d1097e90d28b913903841e6779eb969b62a64706a3eb983e894a5db000fbd89296f45f08885a0e54cd558ef62e81be1165da9be25a6c44920da10f424c + languageName: node + linkType: hard + "for-each@npm:^0.3.3": version: 0.3.3 resolution: "for-each@npm:0.3.3" @@ -7775,6 +8014,23 @@ __metadata: languageName: node linkType: hard +"gauge@npm:^3.0.0": + version: 3.0.2 + resolution: "gauge@npm:3.0.2" + dependencies: + aproba: "npm:^1.0.3 || ^2.0.0" + color-support: "npm:^1.1.2" + console-control-strings: "npm:^1.0.0" + has-unicode: "npm:^2.0.1" + object-assign: "npm:^4.1.1" + signal-exit: "npm:^3.0.0" + string-width: "npm:^4.2.3" + strip-ansi: "npm:^6.0.1" + wide-align: "npm:^1.1.2" + checksum: 46df086451672a5fecd58f7ec86da74542c795f8e00153fbef2884286ce0e86653c3eb23be2d0abb0c4a82b9b2a9dec3b09b6a1cf31c28085fa0376599a26589 + languageName: node + linkType: hard + "gauge@npm:^4.0.3": version: 4.0.4 resolution: "gauge@npm:4.0.4" @@ -8214,6 +8470,16 @@ __metadata: languageName: node linkType: hard +"https-proxy-agent@npm:^7.0.5": + version: 7.0.6 + resolution: "https-proxy-agent@npm:7.0.6" + dependencies: + agent-base: "npm:^7.1.2" + debug: "npm:4" + checksum: 784b628cbd55b25542a9d85033bdfd03d4eda630fb8b3c9477959367f3be95dc476ed2ecbb9836c359c7c698027fc7b45723a302324433590f45d6c1706e8c13 + languageName: node + linkType: hard + "human-signals@npm:^2.1.0": version: 2.1.0 resolution: "human-signals@npm:2.1.0" @@ -9500,6 +9766,13 @@ __metadata: languageName: node linkType: hard +"kafkajs@npm:^2.2.4": + version: 2.2.4 + resolution: "kafkajs@npm:2.2.4" + checksum: 75eb0d221397085f90e51f8a2d752495c9fa9c1b3a1a6db610cd7074fa8c52777f295832fd0a7c49cded5e574337a09fafa8c3f7cf1caa38f4dc9aa20fcfb7df + languageName: node + linkType: hard + "kareem@npm:2.5.1": version: 2.5.1 resolution: "kareem@npm:2.5.1" @@ -9608,12 +9881,15 @@ __metadata: resolution: "koi-server@workspace:app/koi-server" dependencies: "@codegenie/serverless-express": "npm:^4.13.0" + "@confluentinc/kafka-javascript": "npm:^1.2.0" "@nestjs/cli": "npm:^10.3.0" "@nestjs/common": "npm:^10.3.0" "@nestjs/config": "npm:^3.1.1" "@nestjs/core": "npm:^10.3.0" + "@nestjs/microservices": "npm:^11.0.7" "@nestjs/mongoose": "npm:^10.0.2" "@nestjs/platform-express": "npm:^10.3.0" + "@nestjs/schedule": "npm:^5.0.1" "@nestjs/schematics": "npm:^10.0.2" "@nestjs/testing": "npm:^10.3.0" "@toss/date": "npm:^1.1.7" @@ -9646,6 +9922,7 @@ __metadata: feature-nest-poll: "workspace:*" feature-nest-stock: "workspace:*" jest: "npm:29.5.0" + kafkajs: "npm:^2.2.4" mongoose: "npm:^8.1.0" prettier: "npm:^2.6.1" reflect-metadata: "npm:^0.1.13" @@ -9938,6 +10215,13 @@ __metadata: languageName: node linkType: hard +"luxon@npm:~3.5.0": + version: 3.5.0 + resolution: "luxon@npm:3.5.0" + checksum: 48f86e6c1c96815139f8559456a3354a276ba79bcef0ae0d4f2172f7652f3ba2be2237b0e103b8ea0b79b47715354ac9fac04eb1db3485dcc72d5110491dd47f + languageName: node + linkType: hard + "magic-string@npm:0.30.0": version: 0.30.0 resolution: "magic-string@npm:0.30.0" @@ -9956,7 +10240,7 @@ __metadata: languageName: node linkType: hard -"make-dir@npm:^3.0.0": +"make-dir@npm:^3.0.0, make-dir@npm:^3.0.2, make-dir@npm:^3.1.0": version: 3.1.0 resolution: "make-dir@npm:3.1.0" dependencies: @@ -10323,6 +10607,36 @@ __metadata: languageName: node linkType: hard +"mongodb-memory-server-core@npm:10.1.4": + version: 10.1.4 + resolution: "mongodb-memory-server-core@npm:10.1.4" + dependencies: + async-mutex: "npm:^0.5.0" + camelcase: "npm:^6.3.0" + debug: "npm:^4.3.7" + find-cache-dir: "npm:^3.3.2" + follow-redirects: "npm:^1.15.9" + https-proxy-agent: "npm:^7.0.5" + mongodb: "npm:^6.9.0" + new-find-package-json: "npm:^2.0.0" + semver: "npm:^7.6.3" + tar-stream: "npm:^3.1.7" + tslib: "npm:^2.7.0" + yauzl: "npm:^3.1.3" + checksum: 7ee7fd7af7519f00f0540cc5f17753ff1b5088726456d2fb68ec056d35774a37d8f59fb9a246ccc3867edc8602947d289b817b1e156f7f140e9100cc99641848 + languageName: node + linkType: hard + +"mongodb-memory-server@npm:^10.1.4": + version: 10.1.4 + resolution: "mongodb-memory-server@npm:10.1.4" + dependencies: + mongodb-memory-server-core: "npm:10.1.4" + tslib: "npm:^2.7.0" + checksum: 712ae58480ad6d669213640d2265402e51a611f6c68ee7f461f3a961204500e847542ab27a38826d009d8f94fdcf8f6b1073e92c54492f520774098eb51e964e + languageName: node + linkType: hard + "mongodb@npm:6.3.0": version: 6.3.0 resolution: "mongodb@npm:6.3.0" @@ -10357,6 +10671,40 @@ __metadata: languageName: node linkType: hard +"mongodb@npm:^6.9.0": + version: 6.14.2 + resolution: "mongodb@npm:6.14.2" + dependencies: + "@mongodb-js/saslprep": "npm:^1.1.9" + bson: "npm:^6.10.3" + mongodb-connection-string-url: "npm:^3.0.0" + peerDependencies: + "@aws-sdk/credential-providers": ^3.188.0 + "@mongodb-js/zstd": ^1.1.0 || ^2.0.0 + gcp-metadata: ^5.2.0 + kerberos: ^2.0.1 + mongodb-client-encryption: ">=6.0.0 <7" + snappy: ^7.2.2 + socks: ^2.7.1 + peerDependenciesMeta: + "@aws-sdk/credential-providers": + optional: true + "@mongodb-js/zstd": + optional: true + gcp-metadata: + optional: true + kerberos: + optional: true + mongodb-client-encryption: + optional: true + snappy: + optional: true + socks: + optional: true + checksum: 08f6b42b78e9d860c1270300c018e31216d2ad758b0f77ba914274a3dd7cb316d3f6a2353fe1126c712bfb22611abd0389586237341288a64559b7628f07a3bc + languageName: node + linkType: hard + "mongoose@npm:^8.1.0": version: 8.1.0 resolution: "mongoose@npm:8.1.0" @@ -10409,7 +10757,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -10445,6 +10793,15 @@ __metadata: languageName: node linkType: hard +"nan@npm:^2.22.0": + version: 2.22.0 + resolution: "nan@npm:2.22.0" + dependencies: + node-gyp: "npm:latest" + checksum: ab165ba910e549fcc21fd561a33f534d86e81ae36c97b1019dcfe506b09692ff867c97794a54b49c9a83b8b485f529f0f58d24966c3a11863c97dc70814f4d50 + languageName: node + linkType: hard + "nanoid@npm:^3.3.6": version: 3.3.6 resolution: "nanoid@npm:3.3.6" @@ -10491,6 +10848,15 @@ __metadata: languageName: node linkType: hard +"new-find-package-json@npm:^2.0.0": + version: 2.0.0 + resolution: "new-find-package-json@npm:2.0.0" + dependencies: + debug: "npm:^4.3.4" + checksum: 5488ead794bd506894ddd8f3ac6240615e625ce56241ed6ff41a5ff46bdf495a81881bef6d25a3aa16d25f742e86e5629c2d052cd2f60530db3a85b2b1bd146c + languageName: node + linkType: hard + "node-abort-controller@npm:^3.0.1": version: 3.1.1 resolution: "node-abort-controller@npm:3.1.1" @@ -10583,6 +10949,17 @@ __metadata: languageName: node linkType: hard +"nopt@npm:^5.0.0": + version: 5.0.0 + resolution: "nopt@npm:5.0.0" + dependencies: + abbrev: "npm:1" + bin: + nopt: bin/nopt.js + checksum: 00f9bb2d16449469ba8ffcf9b8f0eae6bae285ec74b135fec533e5883563d2400c0cd70902d0a7759e47ac031ccf206ace4e86556da08ed3f1c66dda206e9ccd + languageName: node + linkType: hard + "nopt@npm:^6.0.0": version: 6.0.0 resolution: "nopt@npm:6.0.0" @@ -10619,6 +10996,18 @@ __metadata: languageName: node linkType: hard +"npmlog@npm:^5.0.1": + version: 5.0.1 + resolution: "npmlog@npm:5.0.1" + dependencies: + are-we-there-yet: "npm:^2.0.0" + console-control-strings: "npm:^1.1.0" + gauge: "npm:^3.0.0" + set-blocking: "npm:^2.0.0" + checksum: f42c7b9584cdd26a13c41a21930b6f5912896b6419ab15be88cc5721fc792f1c3dd30eb602b26ae08575694628ba70afdcf3675d86e4f450fc544757e52726ec + languageName: node + linkType: hard + "npmlog@npm:^6.0.0": version: 6.0.2 resolution: "npmlog@npm:6.0.2" @@ -11020,6 +11409,13 @@ __metadata: languageName: node linkType: hard +"pend@npm:~1.2.0": + version: 1.2.0 + resolution: "pend@npm:1.2.0" + checksum: 6c72f5243303d9c60bd98e6446ba7d30ae29e3d56fdb6fae8767e8ba6386f33ee284c97efe3230a0d0217e2b1723b8ab490b1bbf34fcbb2180dbc8a9de47850d + languageName: node + linkType: hard + "picocolors@npm:^1.0.0": version: 1.0.0 resolution: "picocolors@npm:1.0.0" @@ -11055,7 +11451,7 @@ __metadata: languageName: node linkType: hard -"pkg-dir@npm:^4.2.0": +"pkg-dir@npm:^4.1.0, pkg-dir@npm:^4.2.0": version: 4.2.0 resolution: "pkg-dir@npm:4.2.0" dependencies: @@ -12534,6 +12930,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.6.3": + version: 7.7.1 + resolution: "semver@npm:7.7.1" + bin: + semver: bin/semver.js + checksum: 4cfa1eb91ef3751e20fc52e47a935a0118d56d6f15a837ab814da0c150778ba2ca4f1a4d9068b33070ea4273629e615066664c2cfcd7c272caf7a8a0f6518b2c + languageName: node + linkType: hard + "send@npm:0.18.0": version: 0.18.0 resolution: "send@npm:0.18.0" @@ -12821,7 +13226,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": +"signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" checksum: a2f098f247adc367dffc27845853e9959b9e88b01cb301658cfe4194352d8d2bb32e18467c786a7fe15f1d44b233ea35633d076d5e737870b7139949d1ab6318 @@ -13062,6 +13467,20 @@ __metadata: languageName: node linkType: hard +"streamx@npm:^2.15.0": + version: 2.22.0 + resolution: "streamx@npm:2.22.0" + dependencies: + bare-events: "npm:^2.2.0" + fast-fifo: "npm:^1.3.2" + text-decoder: "npm:^1.1.0" + dependenciesMeta: + bare-events: + optional: true + checksum: 9c329bb316e2085e207e471ecd0da18b4ed5b1cfe5cf10e9e7fad3f8f50c6ca1a6a844bdfd9bc7521560b97f229890de82ca162a0e66115300b91a489b1cbefd + languageName: node + linkType: hard + "string-convert@npm:^0.2.0": version: 0.2.1 resolution: "string-convert@npm:0.2.1" @@ -13330,6 +13749,17 @@ __metadata: languageName: node linkType: hard +"tar-stream@npm:^3.1.7": + version: 3.1.7 + resolution: "tar-stream@npm:3.1.7" + dependencies: + b4a: "npm:^1.6.4" + fast-fifo: "npm:^1.2.0" + streamx: "npm:^2.15.0" + checksum: b21a82705a72792544697c410451a4846af1f744176feb0ff11a7c3dd0896961552e3def5e1c9a6bbee4f0ae298b8252a1f4c9381e9f991553b9e4847976f05c + languageName: node + linkType: hard + "tar@npm:^6.1.11, tar@npm:^6.1.2": version: 6.1.15 resolution: "tar@npm:6.1.15" @@ -13391,6 +13821,15 @@ __metadata: languageName: node linkType: hard +"text-decoder@npm:^1.1.0": + version: 1.2.3 + resolution: "text-decoder@npm:1.2.3" + dependencies: + b4a: "npm:^1.6.4" + checksum: bcdec33c0f070aeac38e46e4cafdcd567a58473ed308bdf75260bfbd8f7dc76acbc0b13226afaec4a169d0cb44cec2ab89c57b6395ccf02e941eaebbe19e124a + languageName: node + linkType: hard + "text-segmentation@npm:^1.0.3": version: 1.0.3 resolution: "text-segmentation@npm:1.0.3" @@ -13660,6 +14099,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:2.8.1, tslib@npm:^2.4.0, tslib@npm:^2.7.0": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: 3e2e043d5c2316461cb54e5c7fe02c30ef6dccb3384717ca22ae5c6b5bc95232a6241df19c622d9c73b809bea33b187f6dbc73030963e29950c2141bc32a79f7 + languageName: node + linkType: hard + "tslib@npm:^1.8.1": version: 1.14.1 resolution: "tslib@npm:1.14.1" @@ -14415,7 +14861,7 @@ __metadata: languageName: node linkType: hard -"wide-align@npm:^1.1.5": +"wide-align@npm:^1.1.2, wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" dependencies: @@ -14614,6 +15060,16 @@ __metadata: languageName: node linkType: hard +"yauzl@npm:^3.1.3": + version: 3.2.0 + resolution: "yauzl@npm:3.2.0" + dependencies: + buffer-crc32: "npm:~0.2.3" + pend: "npm:~1.2.0" + checksum: a3cd2bfcf7590673bb35750f2a4e5107e3cc939d32d98a072c0673fe42329e390f471b4a53dbbd72512229099b18aa3b79e6ddb87a73b3a17446080c903a2c4b + languageName: node + linkType: hard + "yn@npm:3.1.1": version: 3.1.1 resolution: "yn@npm:3.1.1"