diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b5a4951 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +AMQP_URL=amqp://localhost:5672 +LOG_LEVEL=debug +TESTS_SKIP_RESILIENCE=0 diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 5f08dfa..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,40 +0,0 @@ -const ignoreForTests = ["test/**/*"]; - -module.exports = { - parser: "@typescript-eslint/parser", - parserOptions: { - project: "tsconfig.json", - sourceType: "module", - }, - plugins: ["jest"], - extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], - env: { - node: true, - jest: true, - "jest/globals": true, - }, - ignorePatterns: [".eslintrc.js"], - rules: { - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": ["error"], - "@typescript-eslint/no-extraneous-class": ["off"], //Nest has this pattern - "@typescript-eslint/ban-types": ["off"], - "no-restricted-imports": [ - "error", - { - patterns: ["*/dist/*"], - }, - ], - "no-warning-comments": ["error"], - }, - overrides: [ - { - files: ignoreForTests, - rules: { - "no-warning-comments": "warn", - "@typescript-eslint/no-empty-function": ["off"], - "no-restricted-syntax": ["off"], - }, - }, - ], -}; diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 14e51a2..7de4aa1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,11 +18,26 @@ jobs: uses: biomejs/setup-biome@v2 with: version: latest + - name: Version + run: biome --version - name: Run lint run: biome ci . test: name: Setup and test runs-on: ubuntu-latest + services: + rabbitmq: + image: rabbitmq:3-management-alpine + env: + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + ports: + - 5672:5672 + - 15672:15672 + env: + AMQP_URL: amqp://guest:guest@127.0.0.1:5672 + LOG_LEVEL: silent + TESTS_SKIP_RESILIENCE: 0 strategy: matrix: node-version: [ '18', '20' ] @@ -35,3 +50,5 @@ jobs: cache: 'npm' - run: npm ci - run: npm run build:test + - run: npm test + diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..b43d411 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,40 @@ +name: Publish package on NPM +on: + push: + tags: + - v* + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + name: Checkout + - uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + cache: 'npm' + - name: publish to npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + REG_TAG_PART="v[[:digit:]]+([[:punct:]]+[[:digit:]]){2}" + REG_TAG="^$REG_TAG_PART$" + REG_TAG_ALPHA="^$REG_TAG_PART-alpha[[:punct:]][[:digit:]]+$" + + VERSION=$GITHUB_REF_NAME + + echo version: $VERSION + + npm ci && npm run build + npm version $VERSION --no-git-tag-version + if [[ $VERSION =~ $REG_TAG ]]; then + npm publish -f + elif [[ $VERSION =~ $REG_TAG_ALPHA ]]; then + npm publish -f --tag alpha + else + echo invalid tag name: $VERSION + exit 1 + fi diff --git a/.gitignore b/.gitignore index eef3e74..6611375 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,31 @@ -dist/ +logs +*.log +npm* +yarn* +.pnpm* +.pnp.* + +.npmrc + *.swp + .DS_Store -node_modules -build/ -xcuserdata/ -DerivedData -.idea -.nyc_output -coverage/ -yarn-error.log .vscode/ + +dist/ +coverage/ +node_modules/ + +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +.env* +!.env.example + +docker-compose.* +!docker-compose.example* diff --git a/.npmignore b/.npmignore index 89d55f4..3e277b6 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,8 @@ * !dist/**/* !asyncapi/**/* +!testing/**/* !types/**/* !pacakge.json !*.md +!LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 2eff0d8..f2ebaed 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # IRIS NodeJS -IRIS module for nodejs. For library integrations see: -- [@globalid/nest-iris](https://github.com/globalid/nest-modules/tree/master/iris) +Install: `npm i @iris-events/iris` + +IRIS module for nodejs. For NestJS integration see [@iris-events/nest-iris](https://github.com/iris-events/nest-iris) diff --git a/biome.json b/biome.json index d5f1614..c66e348 100644 --- a/biome.json +++ b/biome.json @@ -1,25 +1,61 @@ { - "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", - "organizeImports": { - "enabled": true - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true, - "performance": { - "noDelete": "off", - "noAccumulatingSpread": "off" - }, - "suspicious": { - "noExplicitAny": "off", - "noConfusingVoidType": "off" - }, - "complexity": { - "noForEach": "off", - "noBannedTypes": "off" - } - }, - "ignore": ["dist/"] - } + "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", + "organizeImports": { + "enabled": true + }, + "formatter": { + "enabled": true, + "indentWidth": 2, + "indentStyle": "space" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "performance": { + "noDelete": "warn", + "noAccumulatingSpread": "error" + }, + "suspicious": { + "noExplicitAny": "off", + "noConfusingVoidType": "warn", + "noConsoleLog": "error" + }, + "complexity": { + "noForEach": "warn", + "noBannedTypes": "off" + }, + "correctness": { + "noUnusedImports": "error" + }, + "style": { + "useImportType": "off" + } + } + }, + "javascript": { + "parser": { + "unsafeParameterDecoratorsEnabled": true + }, + "formatter": { + "quoteStyle": "single", + "semicolons": "asNeeded" + } + }, + "files": { + "ignore": ["dist/**/*", "coverage/**/*"], + "include": ["./src/**/*", "./test/**/*"] + }, + "overrides": [ + { + "include": ["./test/**/*", "./src/testing/**/*"], + "linter": { + "rules": { + "style": { + "noNonNullAssertion": "off" + } + } + } + } + ] } diff --git a/docker-compose.yml b/docker-compose.example.yml similarity index 92% rename from docker-compose.yml rename to docker-compose.example.yml index 9961f51..6209755 100644 --- a/docker-compose.yml +++ b/docker-compose.example.yml @@ -1,6 +1,5 @@ version: '3.3' services: -#DEPENDENCIES rabbitmq: image: rabbitmq:3-management-alpine container_name: 'rabbitmq' diff --git a/jest-e2e.json b/jest-e2e.json deleted file mode 100644 index 9f77de1..0000000 --- a/jest-e2e.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".spec.(t|j)s$", - "transform": { - "^.+\\.(t|j)s$": "ts-jest" - }, - "collectCoverageFrom": ["src/**/*.ts"], - "coverageDirectory": "coverage-e2e/", - "coverageThreshold": { - "global": { - "branches": 75, - "functions": 88, - "lines": 85, - "statements": 85 - } - }, - "setupFiles": ["dotenv/config"] -} diff --git a/package-lock.json b/package-lock.json index 5fc7863..92a6199 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,39 +1,40 @@ { "name": "@iris-events/iris", - "version": "1.0.0", - "lockfileVersion": 3, + "version": "2.0.0-alpha.2", + "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@iris-events/iris", - "version": "1.0.0", - "license": "ISC", + "version": "2.0.0-alpha.2", + "license": "Apache-2.0", "dependencies": { - "lodash": "^4.17.21", - "uuid": "^9.0.1" - }, - "devDependencies": { - "@biomejs/biome": "1.5.3", - "@types/amqplib": "^0.10.4", - "@types/lodash": "^4.14.202", - "@types/node": "^20.11.16", - "@types/uuid": "^9.0.8", - "rimraf": "^5.0.5", - "ts-node": "^10.9.2" - }, - "peerDependencies": { "amqplib": "*", "class-transformer": "*", "class-validator": "*", "class-validator-jsonschema": "*", - "reflect-metadata": "*" + "lodash": ">=4.17.21", + "reflect-metadata": "^0.2.2" + }, + "devDependencies": { + "@biomejs/biome": "^1.7.1", + "@swc/core": "^1.4.6", + "@types/amqplib": ">=0.10.4", + "@types/lodash": ">=4.14", + "@types/node": ">=18", + "@vitest/coverage-v8": "^1.3.1", + "dotenv": "^16.4.5", + "rimraf": "*", + "ts-node": ">=10.9.2", + "typescript": "^5.4.2", + "unplugin-swc": "^1.4.4", + "vitest": "^1.3.1" } }, "node_modules/@acuminous/bitsyntax": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz", "integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==", - "peer": true, "dependencies": { "buffer-more-ints": "~1.0.0", "debug": "^4.3.4", @@ -43,37 +44,100 @@ "node": ">=0.8" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "node_modules/@biomejs/biome": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.5.3.tgz", - "integrity": "sha512-yvZCa/g3akwTaAQ7PCwPWDCkZs3Qa5ONg/fgOUT9e6wAWsPftCjLQFPXBeGxPK30yZSSpgEmRCfpGTmVbUjGgg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.7.1.tgz", + "integrity": "sha512-wb2UNoFXcgaMdKXKT5ytsYntaogl2FSTjDt20CZynF3v7OXQUcIpTrr+be3XoOGpoZRj3Ytq9TSpmplUREXmeA==", "dev": true, "hasInstallScript": true, "bin": { "biome": "bin/biome" }, "engines": { - "node": ">=14.*" + "node": ">=14.21.3" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.5.3", - "@biomejs/cli-darwin-x64": "1.5.3", - "@biomejs/cli-linux-arm64": "1.5.3", - "@biomejs/cli-linux-arm64-musl": "1.5.3", - "@biomejs/cli-linux-x64": "1.5.3", - "@biomejs/cli-linux-x64-musl": "1.5.3", - "@biomejs/cli-win32-arm64": "1.5.3", - "@biomejs/cli-win32-x64": "1.5.3" + "@biomejs/cli-darwin-arm64": "1.7.1", + "@biomejs/cli-darwin-x64": "1.7.1", + "@biomejs/cli-linux-arm64": "1.7.1", + "@biomejs/cli-linux-arm64-musl": "1.7.1", + "@biomejs/cli-linux-x64": "1.7.1", + "@biomejs/cli-linux-x64-musl": "1.7.1", + "@biomejs/cli-win32-arm64": "1.7.1", + "@biomejs/cli-win32-x64": "1.7.1" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.5.3.tgz", - "integrity": "sha512-ImU7mh1HghEDyqNmxEZBoMPr8SxekkZuYcs+gynKlNW+TALQs7swkERiBLkG9NR0K1B3/2uVzlvYowXrmlW8hw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.7.1.tgz", + "integrity": "sha512-qfLrIIB58dkgiY/1tgG6fSCBK22PZaSIf6blweZBsG6iMij05mEuJt50ne+zPnNFNUmt8t43NC/qOXT3iFHQBA==", "cpu": [ "arm64" ], @@ -83,13 +147,13 @@ "darwin" ], "engines": { - "node": ">=14.*" + "node": ">=14.21.3" } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.5.3.tgz", - "integrity": "sha512-vCdASqYnlpq/swErH7FD6nrFz0czFtK4k/iLgj0/+VmZVjineFPgevOb+Sr9vz0tk0GfdQO60bSpI74zU8M9Dw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.7.1.tgz", + "integrity": "sha512-OGeyNsEcp5VnKbF9/TBjPCTHNEOm7oHegEve07U3KZmzqfpw2Oe3i9DVW8t6vvj1TYbrwWYCld25H34kBDY7Vg==", "cpu": [ "x64" ], @@ -99,13 +163,13 @@ "darwin" ], "engines": { - "node": ">=14.*" + "node": ">=14.21.3" } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.5.3.tgz", - "integrity": "sha512-cupBQv0sNF1OKqBfx7EDWMSsKwRrBUZfjXawT4s6hKV6ALq7p0QzWlxr/sDmbKMLOaLQtw2Qgu/77N9rm+f9Rg==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.7.1.tgz", + "integrity": "sha512-MQDf5wErj1iBvlcxCyOa0XqZYN8WJrupVgbNnqhntO3yVATg8GxduVUn1fDSaolznkDRsj7Pz3Xu1esBFwvfmg==", "cpu": [ "arm64" ], @@ -115,13 +179,13 @@ "linux" ], "engines": { - "node": ">=14.*" + "node": ">=14.21.3" } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.5.3.tgz", - "integrity": "sha512-DYuMizUYUBYfS0IHGjDrOP1RGipqWfMGEvNEJ398zdtmCKLXaUvTimiox5dvx4X15mBK5M2m8wgWUgOP1giUpQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.7.1.tgz", + "integrity": "sha512-giH0/CzLOJ+wbxLxd5Shnr5xQf5fGnTRWLDe3lzjaF7IplVydNCEeZJtncB01SvyA6DAFJsvQ4LNxzAOQfEVCg==", "cpu": [ "arm64" ], @@ -131,13 +195,13 @@ "linux" ], "engines": { - "node": ">=14.*" + "node": ">=14.21.3" } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.5.3.tgz", - "integrity": "sha512-YQrSArQvcv4FYsk7Q91Yv4uuu5F8hJyORVcv3zsjCLGkjIjx2RhjYLpTL733SNL7v33GmOlZY0eFR1ko38tuUw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.7.1.tgz", + "integrity": "sha512-3wmCsGcC3KZ4pfTknXHfyMMlXPMhgfXVAcG5GlrR+Tq2JGiAw0EUydaLpsSBEbcG7IxH6OiUZEJZ95kAycCHBA==", "cpu": [ "x64" ], @@ -147,13 +211,13 @@ "linux" ], "engines": { - "node": ">=14.*" + "node": ">=14.21.3" } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.5.3.tgz", - "integrity": "sha512-UUHiAnlDqr2Y/LpvshBFhUYMWkl2/Jn+bi3U6jKuav0qWbbBKU/ByHgR4+NBxpKBYoCtWxhnmatfH1bpPIuZMw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.7.1.tgz", + "integrity": "sha512-ySNDtPhsLxU125IFHHAxfpoHBpkM56s4mEXeO70GZtgZay/o1h8IUPWCWf5Z7gKgc4jwgYN1U1U9xabI3hZVAg==", "cpu": [ "x64" ], @@ -163,13 +227,13 @@ "linux" ], "engines": { - "node": ">=14.*" + "node": ">=14.21.3" } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.5.3.tgz", - "integrity": "sha512-HxatYH7vf/kX9nrD+pDYuV2GI9GV8EFo6cfKkahAecTuZLPxryHx1WEfJthp5eNsE0+09STGkKIKjirP0ufaZA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.7.1.tgz", + "integrity": "sha512-8hIDakEqZn0i6+388noYKdZ0ZrovTwnvMU/Qp/oJou0G7EPVdXupOe0oxiQSdRN0W7f6CS/yjPCYuVGzDG6r0g==", "cpu": [ "arm64" ], @@ -179,13 +243,13 @@ "win32" ], "engines": { - "node": ">=14.*" + "node": ">=14.21.3" } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.5.3.tgz", - "integrity": "sha512-fMvbSouZEASU7mZH8SIJSANDm5OqsjgtVXlbUqxwed6BP7uuHRSs396Aqwh2+VoW8fwTpp6ybIUoC9FrzB0kyA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.7.1.tgz", + "integrity": "sha512-3W9k3uH6Ea6VOpAS9xkkAlS0LTfnGQjmIUCegZ8SDtK2NgJ1gO+qdEkGJb0ltahusFTN1QxJ107dM7ASA9IUEg==", "cpu": [ "x64" ], @@ -195,7 +259,7 @@ "win32" ], "engines": { - "node": ">=14.*" + "node": ">=14.21.3" } }, "node_modules/@cspotcode/source-map-support": { @@ -210,728 +274,4951 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "dev": true, "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], "dev": true, + "optional": true, + "os": [ + "aix" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=6.0.0" + "node": ">=12" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], "dev": true, "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=14" + "node": ">=12" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true - }, - "node_modules/@types/amqplib": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.4.tgz", - "integrity": "sha512-Y5Sqquh/LqDxSgxYaAAFNM0M7GyONtSDCcFMJk+DQwYEjibPyW6y+Yu9H9omdkKc3epyXULmFN3GTaeBHhn2Hg==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@types/node": "*" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/lodash": { - "version": "4.14.202", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.11.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", - "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "undici-types": "~5.26.4" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true - }, - "node_modules/@types/validator": { - "version": "13.11.8", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.8.tgz", - "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==", - "peer": true - }, - "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], "dev": true, - "bin": { - "acorn": "bin/acorn" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=0.4.0" + "node": ">=12" } }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.4.0" + "node": ">=12" } }, - "node_modules/amqplib": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.3.tgz", - "integrity": "sha512-UHmuSa7n8vVW/a5HGh2nFPqAEr8+cD4dEZ6u9GjP91nHfr1a54RyAKyra7Sb5NH7NBKOUlyQSMXIp0qAixKexw==", - "peer": true, - "dependencies": { - "@acuminous/bitsyntax": "^0.1.2", - "buffer-more-ints": "~1.0.0", - "readable-stream": "1.x >=1.1.9", - "url-parse": "~1.5.10" - }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=12" } }, - "node_modules/arg": { - "version": "4.1.3", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "node_modules/@swc/core": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.6.tgz", + "integrity": "sha512-A7iK9+1qzTCIuc3IYcS8gPHCm9bZVKUJrfNnwveZYyo6OFp3jLno4WOM2yBy5uqedgYATEiWgBYHKq37KrU6IA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@swc/counter": "^0.1.2", + "@swc/types": "^0.1.5" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.4.6", + "@swc/core-darwin-x64": "1.4.6", + "@swc/core-linux-arm-gnueabihf": "1.4.6", + "@swc/core-linux-arm64-gnu": "1.4.6", + "@swc/core-linux-arm64-musl": "1.4.6", + "@swc/core-linux-x64-gnu": "1.4.6", + "@swc/core-linux-x64-musl": "1.4.6", + "@swc/core-win32-arm64-msvc": "1.4.6", + "@swc/core-win32-ia32-msvc": "1.4.6", + "@swc/core-win32-x64-msvc": "1.4.6" + }, + "peerDependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.6.tgz", + "integrity": "sha512-bpggpx/BfLFyy48aUKq1PsNUxb7J6CINlpAUk0V4yXfmGnpZH80Gp1pM3GkFDQyCfq7L7IpjPrIjWQwCrL4hYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.6.tgz", + "integrity": "sha512-vJn+/ZuBTg+vtNkcmgZdH6FQpa0hFVdnB9bAeqYwKkyqP15zaPe6jfC+qL2y/cIeC7ASvHXEKrnCZgBLxfVQ9w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.6.tgz", + "integrity": "sha512-hEmYcB/9XBAl02MtuVHszhNjQpjBzhk/NFulnU33tBMbNZpy2TN5yTsitezMq090QXdDz8sKIALApDyg07ZR8g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.6.tgz", + "integrity": "sha512-/UCYIVoGpm2YVvGHZM2QOA3dexa28BjcpLAIYnoCbgH5f7ulDhE8FAIO/9pasj+kixDBsdqewHfsNXFYlgGJjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.6.tgz", + "integrity": "sha512-LGQsKJ8MA9zZ8xHCkbGkcPSmpkZL2O7drvwsGKynyCttHhpwVjj9lguhD4DWU3+FWIsjvho5Vu0Ggei8OYi/Lw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.6.tgz", + "integrity": "sha512-10JL2nLIreMQDKvq2TECnQe5fCuoqBHu1yW8aChqgHUyg9d7gfZX/kppUsuimqcgRBnS0AjTDAA+JF6UsG/2Yg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.6.tgz", + "integrity": "sha512-EGyjFVzVY6Do89x8sfah7I3cuP4MwtwzmA6OlfD/KASqfCFf5eIaEBMbajgR41bVfMV7lK72lwAIea5xEyq1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.6.tgz", + "integrity": "sha512-gfW9AuXvwSyK07Vb8Y8E9m2oJZk21WqcD+X4BZhkbKB0TCZK0zk1j/HpS2UFlr1JB2zPKPpSWLU3ll0GEHRG2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.6.tgz", + "integrity": "sha512-ZuQm81FhhvNVYtVb9GfZ+Du6e7fZlkisWvuCeBeRiyseNt1tcrQ8J3V67jD2nxje8CVXrwG3oUIbPcybv2rxfQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.6.tgz", + "integrity": "sha512-UagPb7w5V0uzWSjrXwOavGa7s9iv3wrVdEgWy+/inm0OwY4lj3zpK9qDnMWAwYLuFwkI3UG4Q3dH8wD+CUUcjw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "node_modules/@swc/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", + "dev": true + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/amqplib": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.5.tgz", + "integrity": "sha512-/cSykxROY7BWwDoi4Y4/jLAuZTshZxd8Ey1QYa/VaXriMotBDoou7V/twJiOSHzU6t1Kp1AHAUXGCgqq+6DNeg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.11.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", + "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/validator": { + "version": "13.11.9", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", + "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==" + }, + "node_modules/@vitest/coverage-v8": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz", + "integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.3", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "1.3.1" + } + }, + "node_modules/@vitest/expect": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", + "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", + "dev": true, + "dependencies": { + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", + "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", + "dev": true, + "dependencies": { + "@vitest/utils": "1.3.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", + "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", + "dev": true, + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", + "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", + "dev": true, + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", + "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", + "dev": true, + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/amqplib": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.3.tgz", + "integrity": "sha512-UHmuSa7n8vVW/a5HGh2nFPqAEr8+cD4dEZ6u9GjP91nHfr1a54RyAKyra7Sb5NH7NBKOUlyQSMXIp0qAixKexw==", + "dependencies": { + "@acuminous/bitsyntax": "^0.1.2", + "buffer-more-ints": "~1.0.0", + "readable-stream": "1.x >=1.1.9", + "url-parse": "~1.5.10" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-more-ints": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, + "node_modules/class-validator-jsonschema": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/class-validator-jsonschema/-/class-validator-jsonschema-5.0.0.tgz", + "integrity": "sha512-F1skc5+NHZUxtVH56js1wdPKayUoIEZNpiZeNYIAJO0L6hCODmlX+lXwb26RRqTrjo0U24tNkSujn/G0zOvZoQ==", + "dependencies": { + "lodash.groupby": "^4.6.0", + "lodash.merge": "^4.6.2", + "openapi3-ts": "^3.0.0", + "reflect-metadata": "^0.1.13", + "tslib": "^2.4.1" + }, + "peerDependencies": { + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.14.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.3.10", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", + "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.5", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", + "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-tokens": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", + "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", + "dev": true + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true + }, + "node_modules/libphonenumber-js": { + "version": "1.10.58", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.58.tgz", + "integrity": "sha512-53A0IpJFL9LdHbpeatwizf8KSwPICrqn9H0g3Y7WQ+Jgeu9cQ4Ew3WrRtrLBu/CX2lXd5+rgT01/tGlkbkzOjw==" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/magicast": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.3.tgz", + "integrity": "sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "source-map-js": "^1.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", + "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.0.3", + "ufo": "^1.3.2" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi3-ts": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.2.0.tgz", + "integrity": "sha512-/ykNWRV5Qs0Nwq7Pc0nJ78fgILvOT/60OxEmB3v7yQ8a8Bwcm43D4diaYazG/KBn6czA+52XYy931WFLMCUeSg==", + "dependencies": { + "yaml": "^2.2.1" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/rimraf": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", + "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", + "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", + "dev": true, + "dependencies": { + "js-tokens": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tinybench": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", + "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", + "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", + "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", + "dev": true + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/unplugin": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.9.0.tgz", + "integrity": "sha512-14PslvMY3gNbXnQtNIRB566Q057L5Fe7f5LDEamxVi0QQVxoz5hrveBwwZLcKyHtZ09ysmipxRRj5Lv+BGz2Iw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.3", + "chokidar": "^3.6.0", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.6.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-swc": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.4.4.tgz", + "integrity": "sha512-S2mgLIQVNR1+UGIk379/wD3tmkTJfm9QJFyZgXutMDNsSJrcPNJUdSXUNGE/+1Zde9i/I0r0BvDqxGgTkg+eJQ==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "load-tsconfig": "^0.2.5", + "unplugin": "^1.5.1" + }, + "peerDependencies": { + "@swc/core": "^1.2.108" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vite": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz", + "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", + "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", + "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", + "dev": true, + "dependencies": { + "@vitest/expect": "1.3.1", + "@vitest/runner": "1.3.1", + "@vitest/snapshot": "1.3.1", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.2", + "vite": "^5.0.0", + "vite-node": "1.3.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.3.1", + "@vitest/ui": "1.3.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz", + "integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@acuminous/bitsyntax": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@acuminous/bitsyntax/-/bitsyntax-0.1.2.tgz", + "integrity": "sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==", + "requires": { + "buffer-more-ints": "~1.0.0", + "debug": "^4.3.4", + "safe-buffer": "~5.1.2" + } + }, + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@babel/helper-string-parser": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/parser": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "dev": true + }, + "@babel/types": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@biomejs/biome": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.7.1.tgz", + "integrity": "sha512-wb2UNoFXcgaMdKXKT5ytsYntaogl2FSTjDt20CZynF3v7OXQUcIpTrr+be3XoOGpoZRj3Ytq9TSpmplUREXmeA==", + "dev": true, + "requires": { + "@biomejs/cli-darwin-arm64": "1.7.1", + "@biomejs/cli-darwin-x64": "1.7.1", + "@biomejs/cli-linux-arm64": "1.7.1", + "@biomejs/cli-linux-arm64-musl": "1.7.1", + "@biomejs/cli-linux-x64": "1.7.1", + "@biomejs/cli-linux-x64-musl": "1.7.1", + "@biomejs/cli-win32-arm64": "1.7.1", + "@biomejs/cli-win32-x64": "1.7.1" + } + }, + "@biomejs/cli-darwin-arm64": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.7.1.tgz", + "integrity": "sha512-qfLrIIB58dkgiY/1tgG6fSCBK22PZaSIf6blweZBsG6iMij05mEuJt50ne+zPnNFNUmt8t43NC/qOXT3iFHQBA==", + "dev": true, + "optional": true + }, + "@biomejs/cli-darwin-x64": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.7.1.tgz", + "integrity": "sha512-OGeyNsEcp5VnKbF9/TBjPCTHNEOm7oHegEve07U3KZmzqfpw2Oe3i9DVW8t6vvj1TYbrwWYCld25H34kBDY7Vg==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-arm64": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.7.1.tgz", + "integrity": "sha512-MQDf5wErj1iBvlcxCyOa0XqZYN8WJrupVgbNnqhntO3yVATg8GxduVUn1fDSaolznkDRsj7Pz3Xu1esBFwvfmg==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-arm64-musl": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.7.1.tgz", + "integrity": "sha512-giH0/CzLOJ+wbxLxd5Shnr5xQf5fGnTRWLDe3lzjaF7IplVydNCEeZJtncB01SvyA6DAFJsvQ4LNxzAOQfEVCg==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-x64": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.7.1.tgz", + "integrity": "sha512-3wmCsGcC3KZ4pfTknXHfyMMlXPMhgfXVAcG5GlrR+Tq2JGiAw0EUydaLpsSBEbcG7IxH6OiUZEJZ95kAycCHBA==", + "dev": true, + "optional": true + }, + "@biomejs/cli-linux-x64-musl": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.7.1.tgz", + "integrity": "sha512-ySNDtPhsLxU125IFHHAxfpoHBpkM56s4mEXeO70GZtgZay/o1h8IUPWCWf5Z7gKgc4jwgYN1U1U9xabI3hZVAg==", + "dev": true, + "optional": true + }, + "@biomejs/cli-win32-arm64": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.7.1.tgz", + "integrity": "sha512-8hIDakEqZn0i6+388noYKdZ0ZrovTwnvMU/Qp/oJou0G7EPVdXupOe0oxiQSdRN0W7f6CS/yjPCYuVGzDG6r0g==", + "dev": true, + "optional": true + }, + "@biomejs/cli-win32-x64": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.7.1.tgz", + "integrity": "sha512-3W9k3uH6Ea6VOpAS9xkkAlS0LTfnGQjmIUCegZ8SDtK2NgJ1gO+qdEkGJb0ltahusFTN1QxJ107dM7ASA9IUEg==", + "dev": true, + "optional": true + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } + } + }, + "@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "dev": true, + "optional": true + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.27.8" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "dependencies": { + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + } + } + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", + "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", + "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", + "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", + "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", + "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", + "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", + "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", + "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", + "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", + "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", + "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", + "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", + "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "dev": true, + "optional": true + }, + "@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true + }, + "@swc/core": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.6.tgz", + "integrity": "sha512-A7iK9+1qzTCIuc3IYcS8gPHCm9bZVKUJrfNnwveZYyo6OFp3jLno4WOM2yBy5uqedgYATEiWgBYHKq37KrU6IA==", + "dev": true, + "requires": { + "@swc/core-darwin-arm64": "1.4.6", + "@swc/core-darwin-x64": "1.4.6", + "@swc/core-linux-arm-gnueabihf": "1.4.6", + "@swc/core-linux-arm64-gnu": "1.4.6", + "@swc/core-linux-arm64-musl": "1.4.6", + "@swc/core-linux-x64-gnu": "1.4.6", + "@swc/core-linux-x64-musl": "1.4.6", + "@swc/core-win32-arm64-msvc": "1.4.6", + "@swc/core-win32-ia32-msvc": "1.4.6", + "@swc/core-win32-x64-msvc": "1.4.6", + "@swc/counter": "^0.1.2", + "@swc/types": "^0.1.5" + } + }, + "@swc/core-darwin-arm64": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.6.tgz", + "integrity": "sha512-bpggpx/BfLFyy48aUKq1PsNUxb7J6CINlpAUk0V4yXfmGnpZH80Gp1pM3GkFDQyCfq7L7IpjPrIjWQwCrL4hYw==", + "dev": true, + "optional": true + }, + "@swc/core-darwin-x64": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.6.tgz", + "integrity": "sha512-vJn+/ZuBTg+vtNkcmgZdH6FQpa0hFVdnB9bAeqYwKkyqP15zaPe6jfC+qL2y/cIeC7ASvHXEKrnCZgBLxfVQ9w==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm-gnueabihf": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.6.tgz", + "integrity": "sha512-hEmYcB/9XBAl02MtuVHszhNjQpjBzhk/NFulnU33tBMbNZpy2TN5yTsitezMq090QXdDz8sKIALApDyg07ZR8g==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm64-gnu": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.6.tgz", + "integrity": "sha512-/UCYIVoGpm2YVvGHZM2QOA3dexa28BjcpLAIYnoCbgH5f7ulDhE8FAIO/9pasj+kixDBsdqewHfsNXFYlgGJjQ==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm64-musl": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.6.tgz", + "integrity": "sha512-LGQsKJ8MA9zZ8xHCkbGkcPSmpkZL2O7drvwsGKynyCttHhpwVjj9lguhD4DWU3+FWIsjvho5Vu0Ggei8OYi/Lw==", + "dev": true, + "optional": true + }, + "@swc/core-linux-x64-gnu": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.6.tgz", + "integrity": "sha512-10JL2nLIreMQDKvq2TECnQe5fCuoqBHu1yW8aChqgHUyg9d7gfZX/kppUsuimqcgRBnS0AjTDAA+JF6UsG/2Yg==", + "dev": true, + "optional": true + }, + "@swc/core-linux-x64-musl": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.6.tgz", + "integrity": "sha512-EGyjFVzVY6Do89x8sfah7I3cuP4MwtwzmA6OlfD/KASqfCFf5eIaEBMbajgR41bVfMV7lK72lwAIea5xEyq1AQ==", + "dev": true, + "optional": true + }, + "@swc/core-win32-arm64-msvc": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.6.tgz", + "integrity": "sha512-gfW9AuXvwSyK07Vb8Y8E9m2oJZk21WqcD+X4BZhkbKB0TCZK0zk1j/HpS2UFlr1JB2zPKPpSWLU3ll0GEHRG2A==", + "dev": true, + "optional": true + }, + "@swc/core-win32-ia32-msvc": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.6.tgz", + "integrity": "sha512-ZuQm81FhhvNVYtVb9GfZ+Du6e7fZlkisWvuCeBeRiyseNt1tcrQ8J3V67jD2nxje8CVXrwG3oUIbPcybv2rxfQ==", + "dev": true, + "optional": true + }, + "@swc/core-win32-x64-msvc": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.6.tgz", + "integrity": "sha512-UagPb7w5V0uzWSjrXwOavGa7s9iv3wrVdEgWy+/inm0OwY4lj3zpK9qDnMWAwYLuFwkI3UG4Q3dH8wD+CUUcjw==", + "dev": true, + "optional": true + }, + "@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "@swc/types": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", + "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", + "dev": true + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "@types/amqplib": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.10.5.tgz", + "integrity": "sha512-/cSykxROY7BWwDoi4Y4/jLAuZTshZxd8Ey1QYa/VaXriMotBDoou7V/twJiOSHzU6t1Kp1AHAUXGCgqq+6DNeg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "@types/lodash": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==", + "dev": true + }, + "@types/node": { + "version": "20.11.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.26.tgz", + "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", + "dev": true, + "requires": { + "undici-types": "~5.26.4" + } + }, + "@types/validator": { + "version": "13.11.9", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", + "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==" + }, + "@vitest/coverage-v8": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz", + "integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.3", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.2.0" + } + }, + "@vitest/expect": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz", + "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==", + "dev": true, + "requires": { + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", + "chai": "^4.3.10" + } + }, + "@vitest/runner": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz", + "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==", + "dev": true, + "requires": { + "@vitest/utils": "1.3.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + } + }, + "@vitest/snapshot": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz", + "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==", + "dev": true, + "requires": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + } + }, + "@vitest/spy": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz", + "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==", + "dev": true, + "requires": { + "tinyspy": "^2.2.0" + } + }, + "@vitest/utils": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz", + "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==", + "dev": true, + "requires": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + } + }, + "acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "dev": true + }, + "acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true + }, + "amqplib": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.3.tgz", + "integrity": "sha512-UHmuSa7n8vVW/a5HGh2nFPqAEr8+cD4dEZ6u9GjP91nHfr1a54RyAKyra7Sb5NH7NBKOUlyQSMXIp0qAixKexw==", + "requires": { + "@acuminous/bitsyntax": "^0.1.2", + "buffer-more-ints": "~1.0.0", + "readable-stream": "1.x >=1.1.9", + "url-parse": "~1.5.10" + } + }, + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", "dev": true }, - "node_modules/balanced-match": { + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/brace-expansion": { + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, - "dependencies": { + "requires": { "balanced-match": "^1.0.0" } }, - "node_modules/buffer-more-ints": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer-more-ints": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", - "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", - "peer": true + "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==" }, - "node_modules/class-transformer": { + "cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true + }, + "chai": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", + "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.0.8" + } + }, + "check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "requires": { + "get-func-name": "^2.0.2" + } + }, + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "class-transformer": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", - "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "peer": true + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" }, - "node_modules/class-validator": { + "class-validator": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", - "peer": true, - "dependencies": { + "requires": { "@types/validator": "^13.11.8", "libphonenumber-js": "^1.10.53", "validator": "^13.9.0" } }, - "node_modules/class-validator-jsonschema": { + "class-validator-jsonschema": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/class-validator-jsonschema/-/class-validator-jsonschema-5.0.0.tgz", "integrity": "sha512-F1skc5+NHZUxtVH56js1wdPKayUoIEZNpiZeNYIAJO0L6hCODmlX+lXwb26RRqTrjo0U24tNkSujn/G0zOvZoQ==", - "peer": true, - "dependencies": { + "requires": { "lodash.groupby": "^4.6.0", "lodash.merge": "^4.6.2", "openapi3-ts": "^3.0.0", - "reflect-metadata": "^0.1.13", + "reflect-metadata": "^0.2.2", "tslib": "^2.4.1" - }, - "peerDependencies": { - "class-transformer": "^0.4.0 || ^0.5.0", - "class-validator": "^0.14.0" } }, - "node_modules/class-validator-jsonschema/node_modules/reflect-metadata": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", - "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==", - "peer": true - }, - "node_modules/color-convert": { + "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "dependencies": { + "requires": { "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" } }, - "node_modules/color-name": { + "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/core-util-is": { + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "peer": true + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" }, - "node_modules/create-require": { + "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, - "node_modules/cross-spawn": { + "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, - "dependencies": { + "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" } }, - "node_modules/debug": { + "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "peer": true, - "dependencies": { + "requires": { "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } } }, - "node_modules/diff": { + "deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } + "dev": true }, - "node_modules/eastasianwidth": { + "diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true + }, + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true + }, + "eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, - "node_modules/emoji-regex": { + "emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, - "node_modules/foreground-child": { + "esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0" + } + }, + "execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "dev": true, - "dependencies": { + "requires": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob": { + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "optional": true + }, + "get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true + }, + "get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true + }, + "glob": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", "dev": true, - "dependencies": { + "requires": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", "minimatch": "^9.0.1", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/inherits": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "peer": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "node_modules/is-fullwidth-code-point": { + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, - "engines": { - "node": ">=8" + "requires": { + "is-extglob": "^2.1.1" } }, - "node_modules/isarray": { + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "peer": true + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" }, - "node_modules/isexe": { + "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/jackspeak": { + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jackspeak": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { + "requires": { + "@isaacs/cliui": "^8.0.2", "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/libphonenumber-js": { - "version": "1.10.54", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.54.tgz", - "integrity": "sha512-P+38dUgJsmh0gzoRDoM4F5jLbyfztkU6PY6eSK6S5HwTi/LPvnwXqVCQZlAy1FxZ5c48q25QhxGQ0pq+WQcSlQ==", - "peer": true + "js-tokens": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", + "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", + "dev": true }, - "node_modules/lodash": { + "jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true + }, + "libphonenumber-js": { + "version": "1.10.58", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.58.tgz", + "integrity": "sha512-53A0IpJFL9LdHbpeatwizf8KSwPICrqn9H0g3Y7WQ+Jgeu9cQ4Ew3WrRtrLBu/CX2lXd5+rgT01/tGlkbkzOjw==" + }, + "load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true + }, + "local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "requires": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + } + }, + "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, - "node_modules/lodash.groupby": { + "lodash.groupby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", - "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", - "peer": true + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" }, - "node_modules/lodash.merge": { + "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "peer": true + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, - "node_modules/lru-cache": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz", - "integrity": "sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==", + "loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, - "engines": { - "node": "14 || >=16.14" + "requires": { + "get-func-name": "^2.0.1" } }, - "node_modules/make-error": { + "lru-cache": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true + }, + "magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "magicast": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.3.tgz", + "integrity": "sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==", + "dev": true, + "requires": { + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "source-map-js": "^1.0.2" + } + }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, + "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, - "node_modules/minimatch": { + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true + }, + "minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, - "dependencies": { + "requires": { "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minipass": { + "minipass": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true + }, + "mlly": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", + "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==", "dev": true, - "engines": { - "node": ">=16 || 14 >=14.17" + "requires": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.0.3", + "ufo": "^1.3.2" } }, - "node_modules/ms": { + "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "peer": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/openapi3-ts": { + "nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "requires": { + "path-key": "^4.0.0" + }, + "dependencies": { + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } + }, + "openapi3-ts": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-3.2.0.tgz", "integrity": "sha512-/ykNWRV5Qs0Nwq7Pc0nJ78fgILvOT/60OxEmB3v7yQ8a8Bwcm43D4diaYazG/KBn6czA+52XYy931WFLMCUeSg==", - "peer": true, - "dependencies": { + "requires": { "yaml": "^2.2.1" } }, - "node_modules/path-key": { + "p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "engines": { - "node": ">=8" - } + "dev": true }, - "node_modules/path-scurry": { + "path-scurry": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", "dev": true, - "dependencies": { + "requires": { "lru-cache": "^9.1.1 || ^10.0.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/querystringify": { + "pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dev": true, + "requires": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "dev": true, + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + } + }, + "pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "requires": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + } + }, + "querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "peer": true + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" }, - "node_modules/readable-stream": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, + "readable-stream": { "version": "1.1.14", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", - "peer": true, - "dependencies": { + "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "0.0.1", "string_decoder": "~0.10.x" } }, - "node_modules/reflect-metadata": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", - "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==", - "peer": true + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } }, - "node_modules/requires-port": { + "reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "peer": true + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, - "node_modules/rimraf": { + "rimraf": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", "dev": true, - "dependencies": { + "requires": { "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/safe-buffer": { + "rollup": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", + "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.13.0", + "@rollup/rollup-android-arm64": "4.13.0", + "@rollup/rollup-darwin-arm64": "4.13.0", + "@rollup/rollup-darwin-x64": "4.13.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", + "@rollup/rollup-linux-arm64-gnu": "4.13.0", + "@rollup/rollup-linux-arm64-musl": "4.13.0", + "@rollup/rollup-linux-riscv64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-gnu": "4.13.0", + "@rollup/rollup-linux-x64-musl": "4.13.0", + "@rollup/rollup-win32-arm64-msvc": "4.13.0", + "@rollup/rollup-win32-ia32-msvc": "4.13.0", + "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@types/estree": "1.0.5", + "fsevents": "~2.3.2" + } + }, + "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "peer": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "node_modules/shebang-command": { + "semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "dependencies": { + "requires": { "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" } }, - "node_modules/shebang-regex": { + "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "engines": { - "node": ">=8" - } + "dev": true }, - "node_modules/signal-exit": { + "siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "dev": true }, - "node_modules/string_decoder": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true + }, + "string_decoder": { "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "peer": true + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" }, - "node_modules/string-width": { + "string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, - "dependencies": { + "requires": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", + "string-width-cjs": { + "version": "npm:string-width@4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "dependencies": { + "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } } }, - "node_modules/string-width/node_modules/strip-ansi": { + "strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, - "dependencies": { + "requires": { "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "dependencies": { + "requires": { "ansi-regex": "^5.0.1" }, - "engines": { - "node": ">=8" + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + } } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + }, + "strip-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", + "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" + "requires": { + "js-tokens": "^8.0.2" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" }, - "engines": { - "node": ">=8" + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, - "node_modules/ts-node": { + "tinybench": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", + "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", + "dev": true + }, + "tinypool": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", + "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", + "dev": true + }, + "tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, - "dependencies": { + "requires": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", @@ -945,219 +5232,276 @@ "make-error": "^1.1.1", "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } } }, - "node_modules/tslib": { + "tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "peer": true + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, - "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true }, - "node_modules/undici-types": { + "typescript": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", + "dev": true + }, + "ufo": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", + "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", + "dev": true + }, + "undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true }, - "node_modules/url-parse": { + "unplugin": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.9.0.tgz", + "integrity": "sha512-14PslvMY3gNbXnQtNIRB566Q057L5Fe7f5LDEamxVi0QQVxoz5hrveBwwZLcKyHtZ09ysmipxRRj5Lv+BGz2Iw==", + "dev": true, + "requires": { + "acorn": "^8.11.3", + "chokidar": "^3.6.0", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.6.1" + } + }, + "unplugin-swc": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.4.4.tgz", + "integrity": "sha512-S2mgLIQVNR1+UGIk379/wD3tmkTJfm9QJFyZgXutMDNsSJrcPNJUdSXUNGE/+1Zde9i/I0r0BvDqxGgTkg+eJQ==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.1.0", + "load-tsconfig": "^0.2.5", + "unplugin": "^1.5.1" + } + }, + "url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "peer": true, - "dependencies": { + "requires": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { + "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, - "node_modules/validator": { + "v8-to-istanbul": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, + "validator": { "version": "13.11.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", - "peer": true, - "engines": { - "node": ">= 0.10" + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" + }, + "vite": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz", + "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==", + "dev": true, + "requires": { + "esbuild": "^0.19.3", + "fsevents": "~2.3.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" } }, - "node_modules/which": { + "vite-node": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz", + "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==", + "dev": true, + "requires": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + } + }, + "vitest": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz", + "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==", + "dev": true, + "requires": { + "@vitest/expect": "1.3.1", + "@vitest/runner": "1.3.1", + "@vitest/snapshot": "1.3.1", + "@vitest/spy": "1.3.1", + "@vitest/utils": "1.3.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.2", + "vite": "^5.0.0", + "vite-node": "1.3.1", + "why-is-node-running": "^2.2.2" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "webpack-virtual-modules": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.1.tgz", + "integrity": "sha512-poXpCylU7ExuvZK8z+On3kX+S8o/2dQ/SVYueKA0D4WEMXROXgY8Ez50/bQEUmvoSMMrWcrJqCHuhAbsiwg7Dg==", + "dev": true + }, + "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "dependencies": { + "requires": { "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" } }, - "node_modules/wrap-ansi": { + "why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dev": true, + "requires": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + } + }, + "wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, - "dependencies": { + "requires": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "dependencies": { + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + } } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "dependencies": { + "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, - "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", - "peer": true, - "engines": { - "node": ">= 14" - } + "yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==" }, - "node_modules/yn": { + "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "engines": { - "node": ">=6" - } + "dev": true + }, + "yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true } } } diff --git a/package.json b/package.json index e8e4a54..3694494 100644 --- a/package.json +++ b/package.json @@ -1,37 +1,54 @@ { "name": "@iris-events/iris", - "version": "1.0.0", - "description": "", + "version": "2.0.0", + "description": "NodeJS module for iris-events", "main": "dist/index", "types": "dist/index.d.ts", + "author": "GlobaliD", + "license": "Apache-2.0", + "keywords": [ + "iris iris-events" + ], + "repository": { + "type": "git", + "url": "https://github.com/iris-events/iris-node" + }, "scripts": { "build": "rimraf dist && tsc -p tsconfig.build.json", "build:test": "rimraf dist_all_tmp && tsc -p tsconfig.build-test.json && rimraf dist_all_tmp", + "lint:checkerrors": "biome check . --diagnostic-level=error", "lint": "biome check .", + "lint:ci": "biome ci .", "lint:fix": "biome check --apply .", - "test": "exit 0" + "test": "LOG_LEVEL=silent vitest --run", + "test:debug": "LOG_LEVEL=debug vitest --run --coverage false" }, - "keywords": [], - "author": "", - "license": "ISC", "dependencies": { - "lodash": "^4.17.21", - "uuid": "^9.0.1" - }, - "peerDependencies": { + "lodash": ">=4.17.21", "amqplib": "*", "class-transformer": "*", "class-validator": "*", "class-validator-jsonschema": "*", - "reflect-metadata": "*" + "reflect-metadata": "^0.2.2" }, "devDependencies": { - "@biomejs/biome": "1.5.3", - "@types/amqplib": "^0.10.4", - "@types/lodash": "^4.14.202", - "@types/node": "^20.11.16", - "@types/uuid": "^9.0.8", - "rimraf": "^5.0.5", - "ts-node": "^10.9.2" + "@biomejs/biome": "^1.7.1", + "@swc/core": "^1.4.6", + "@types/amqplib": ">=0.10.4", + "@types/lodash": ">=4.14", + "@types/node": ">=18", + "@vitest/coverage-v8": "^1.3.1", + "dotenv": "^16.4.5", + "rimraf": "*", + "ts-node": ">=10.9.2", + "typescript": "^5.4.2", + "unplugin-swc": "^1.4.4", + "vitest": "^1.3.1" + }, + "overrides": { + "reflect-metadata": "^0.2.2", + "class-transformer": "*", + "class-validator": "*", + "class-validator-jsonschema": "*" } } diff --git a/src/index.ts b/src/index.ts index bb1fa1d..d9b7e58 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,24 +1,26 @@ -import "reflect-metadata"; +import 'reflect-metadata' -export * from "./lib/message"; -export * from "./lib/message_handler"; -export * from "./lib/subscription.decorator"; +export * from './lib/message' +export * from './lib/message_handler' +export * from './lib/subscription.decorator' -export * as storage from "./lib/storage"; -export * as registerProcessed from "./lib/register.processed"; -export * as validation from "./lib/validation"; -export * as publish from "./lib/publish"; -export * as amqpHelper from "./lib/amqp.helper"; -export * as message from "./lib/message"; -export * as messageHandler from "./lib/message_handler"; -export * as consume from "./lib/consume"; -export * as helper from "./lib/helper"; -export * as subscription from "./lib/subscription"; -export { connection, Connection, ConnectionConfigI } from "./lib/connection"; -export * as errors from "./lib/errors"; -export * from "./lib/subscription.messages"; -export * as constants from "./lib/constants"; -export { default as flags } from "./lib/flags"; -export * as featManagement from "./lib/feat.management"; -export * as paramDecorators from "./lib/message_handler.param.decorator"; -export * as integration from "./integration"; +export * as storage from './lib/storage' +export * as registerProcessed from './lib/register.processed' +export * as validation from './lib/validation' +export * as publish from './lib/publish' +export * as amqpHelper from './lib/amqp.helper' +export * as message from './lib/message' +export * as messageHandler from './lib/message_handler' +export * as consume from './lib/consume' +export * as helper from './lib/helper' +export * as subscription from './lib/subscription' +export { connection, Connection, ConnectionConfigI } from './lib/connection' +export * as errors from './lib/errors' +export * from './lib/subscription.messages' +export * as constants from './lib/constants' +export { default as flags } from './lib/flags' +export * as featManagement from './lib/feat.management' +export * as paramDecorators from './lib/message_handler.param.decorator' +export * as mdc from './lib/mdc' +export * as integration from './integration' +export * as logger from './logger' diff --git a/src/integration.ts b/src/integration.ts index 5b38955..0cd32bb 100644 --- a/src/integration.ts +++ b/src/integration.ts @@ -1,30 +1,29 @@ -import { ClassConstructor } from "class-transformer"; import { - CustomChannelClassesI, - IrisChannels, -} from "./lib/asyncapi/schema/channels"; -import { CustomErrorI, registerRejectableErrors } from "./lib/errors"; -import { LoggerI, loggers } from "./logger"; + type CustomChannelClassesI, + IrisChannels, +} from './lib/asyncapi/schema/channels' +import { type CustomErrorI, registerRejectableErrors } from './lib/errors' +import Logger, { type LoggerI } from './logger' export interface IrisIntegrationI { - customLoggerClass?: ClassConstructor; - rejectableErrors?: CustomErrorI[]; - asyncapi?: { - customChannelClasses?: CustomChannelClassesI; - }; + customLoggerInstance?: LoggerI + rejectableErrors?: CustomErrorI[] + asyncapi?: { + customChannelClasses?: CustomChannelClassesI + } } /** * Override internals when integrating IRIS with a specific platorm */ -export function init(config: IrisIntegrationI): void { - if (config.rejectableErrors !== undefined) { - registerRejectableErrors(config.rejectableErrors); - } - if (config.customLoggerClass !== undefined) { - loggers.setLogger(config.customLoggerClass); - } - if (config.asyncapi?.customChannelClasses !== undefined) { - IrisChannels.setCustomChannelClasses(config.asyncapi.customChannelClasses); - } +export function update(config: IrisIntegrationI): void { + if (config.rejectableErrors !== undefined) { + registerRejectableErrors(config.rejectableErrors) + } + if (config.customLoggerInstance !== undefined) { + Logger.replaceLogger(config.customLoggerInstance) + } + if (config.asyncapi?.customChannelClasses !== undefined) { + IrisChannels.setCustomChannelClasses(config.asyncapi.customChannelClasses) + } } diff --git a/src/lib/amqp.helper.ts b/src/lib/amqp.helper.ts index 4f77ad4..adfd6b5 100644 --- a/src/lib/amqp.helper.ts +++ b/src/lib/amqp.helper.ts @@ -1,140 +1,152 @@ -import * as amqplib from "amqplib"; -import * as amqplibDefs from "amqplib/lib/defs"; -import _ from "lodash"; -import { getLogger } from "../logger"; -import { connection } from "./connection"; -import { MANAGED_EXCHANGES, MESSAGE_HEADERS } from "./constants"; -import * as helper from "./helper"; -import * as messageI from "./message.interfaces"; - -const { FRONTEND } = MANAGED_EXCHANGES; - -const logger = getLogger("Iris:IrisHelper"); +import type * as amqplib from 'amqplib' +import * as amqplibDefs from 'amqplib/lib/defs' +import _ from 'lodash' +import logger from '../logger' +import { connection } from './connection' +import { MANAGED_EXCHANGES, MESSAGE_HEADERS } from './constants' +import { asError } from './errors' +import * as helper from './helper' +import type * as messageI from './message.interfaces' + +const { FRONTEND } = MANAGED_EXCHANGES + +const TAG = 'Iris:IrisHelper' + +export type MessagePropertiesWithHeadersI = amqplib.MessageProperties & { + headers: amqplib.MessagePropertyHeaders +} export const getFrontendQueueName = (): string => - `${helper.getServiceName()}.${FRONTEND.SUFFIX}`; + `${helper.getServiceName()}.${FRONTEND.SUFFIX}` export async function assertQueue( - queueName: string, - options?: amqplib.Options.AssertQueue, + queueName: string, + options?: amqplib.Options.AssertQueue, ): Promise { - const channelTag = "channel-create"; - let channel = await getTemporaryChannel(channelTag); - - try { - logger.log("AssertQueue", { options, queueName }); - await channel.assertQueue(queueName, options); - - return; - } catch (e) { - if ( - (<{ code: number }>e).code !== amqplibDefs.constants.PRECONDITION_FAILED - ) { - throw e; - } - - logger.warn( - `AssertQueue ${queueName} can not be asserted: ${(e).message}`, - ); - - // channel was closed, open a new one again - channel = await getTemporaryChannel(channelTag); - } - - const qCheck = await channel.checkQueue(queueName); - if (qCheck.messageCount < 1) { - logger.log( - `AssertQueue ${queueName} recreating queue with new configuration`, - ); - await channel.deleteQueue(queueName); - await channel.assertQueue(queueName, options); - } + const channelTag = 'channel-create' + let channel = await getTemporaryChannel(channelTag) + + try { + logger.debug(TAG, `AssertQueue ${queueName}`, { options, queueName }) + await channel.assertQueue(queueName, options) + + return + } catch (err) { + if ( + (<{ code: number }>err).code !== amqplibDefs.constants.PRECONDITION_FAILED + ) { + throw err + } + + logger.warn( + TAG, + `AssertQueue ${queueName} can not be asserted: ${asError(err).message}`, + ) + + // channel was closed, open a new one again + channel = await getTemporaryChannel(channelTag) + } + + const qCheck = await channel.checkQueue(queueName) + if (qCheck.messageCount < 1) { + logger.debug( + TAG, + `AssertQueue ${queueName} recreating queue with new configuration`, + ) + await channel.deleteQueue(queueName) + await channel.assertQueue(queueName, options) + } } export async function assertExchange( - exchangeName: string, - exchangeType: messageI.ExchangeType, - options?: amqplib.Options.AssertExchange, + exchangeName: string, + exchangeType: messageI.ExchangeType, + options?: amqplib.Options.AssertExchange, ): Promise { - const channelTag = "exchange-create"; - const channel = await getTemporaryChannel(channelTag); - - const onErr = _.noop; - channel.on("error", onErr); - - try { - logger.log("AssertExchange", { exchangeName, options, exchangeType }); - await channel.assertExchange(exchangeName, exchangeType, options); - channel.off("error", onErr); - - return; - } catch (e) { - if ( - (<{ code: number }>e).code !== amqplibDefs.constants.PRECONDITION_FAILED - ) { - throw e; - } - - logger.warn( - `AssertExchange ${exchangeName} can not be asserted: ${ - (e).message - }. Will use one with existing settings.`, - ); - } + const channelTag = 'exchange-create' + const channel = await getTemporaryChannel(channelTag) + + const onErr = _.noop + channel.on('error', onErr) + + try { + logger.debug(TAG, `AssertExchange: ${exchangeName}`, { + exchangeName, + options, + exchangeType, + }) + await channel.assertExchange(exchangeName, exchangeType, options) + channel.off('error', onErr) + + return + } catch (err) { + if ( + (<{ code: number }>err).code !== amqplibDefs.constants.PRECONDITION_FAILED + ) { + throw err + } + + logger.warn( + TAG, + `AssertExchange ${exchangeName} can not be asserted: ${ + asError(err).message + }. Will use one with existing settings.`, + ) + } } export async function getTemporaryChannel( - tag: string, + tag: string, ): Promise { - const channel = await connection.assureChannel(tag); - const onErr = _.noop; - // remove error listener from before if channel was not closed yet - channel.off("error", onErr); - channel.on("error", onErr); + const channel = await connection.assureChannel(tag) + const onErr = _.noop + // remove error listener from before if channel was not closed yet + channel.off('error', onErr) + channel.on('error', onErr) - return channel; + return channel } export function safeAmqpObjectForLogging< - T extends - | undefined - | amqplib.Message - | amqplib.Options.Publish - | amqplib.MessagePropertyHeaders, + T extends + | undefined + | amqplib.Message + | amqplib.Options.Publish + | amqplib.MessagePropertyHeaders, >(msg: T): T { - if (msg === undefined) { - return msg; - } - - const jwtHeader = MESSAGE_HEADERS.MESSAGE.JWT; - const jwtPath: string | undefined = [ - `properties.headers[${jwtHeader}]`, - `headers[${jwtHeader}]`, - jwtHeader, - ].find((jp) => _.has(msg, jp)); - - if (jwtPath === undefined) { - return msg; - } - - return _.chain({}).merge(msg).set(jwtPath, "").value(); + if (msg === undefined) { + return msg + } + + const jwtHeader = MESSAGE_HEADERS.MESSAGE.JWT + const jwtPath: string | undefined = [ + `properties.headers[${jwtHeader}]`, + `headers[${jwtHeader}]`, + jwtHeader, + ].find((jp) => _.has(msg, jp)) + + if (jwtPath === undefined) { + return msg + } + + return _.chain({}).merge(msg).set(jwtPath, '').value() } export function cloneAmqpMsgProperties( - msg: amqplib.ConsumeMessage, -): amqplib.MessageProperties { - // It's happening with redelivered messages that headers are undefined (??) - const msgProperties = _.cloneDeep(msg.properties); - if (_.isNil(msgProperties.headers)) { - msgProperties.headers = {}; - } - - return msgProperties; + msg: amqplib.ConsumeMessage, +): MessagePropertiesWithHeadersI { + // It's happening with redelivered messages that headers are undefined (??) + const msgProperties = _.cloneDeep(msg.properties) + if (_.isNil(msgProperties.headers)) { + msgProperties.headers = {} + } + + return msgProperties } export function hasClientContext(msg: amqplib.ConsumeMessage): boolean { - const lookupKey = `properties.headers[${MESSAGE_HEADERS.MESSAGE.SESSION_ID}]`; - const hasSession = _.get(msg, lookupKey); + const lookupKey = `properties.headers[${MESSAGE_HEADERS.MESSAGE.SESSION_ID}]` + const hasSession = _.get(msg, lookupKey) - return hasSession !== undefined; + return hasSession !== undefined } diff --git a/src/lib/asyncapi/class_validator/converters.ts b/src/lib/asyncapi/class_validator/converters.ts index a7f0838..669a3b2 100644 --- a/src/lib/asyncapi/class_validator/converters.ts +++ b/src/lib/asyncapi/class_validator/converters.ts @@ -1,32 +1,33 @@ -import { ClassConstructor } from "class-transformer"; -import { IS_INSTANCE, ValidationTypes } from "class-validator"; -import { IOptions } from "class-validator-jsonschema/build/options"; -import { ValidationMetadata } from "class-validator/types/metadata/ValidationMetadata"; -import { ReferenceObject, SchemaObject } from "../interfaces"; -import { getGenerator } from "./custom_generators"; +import type { ClassConstructor } from 'class-transformer' +import { IS_INSTANCE, ValidationTypes } from 'class-validator' +import type { IOptions } from 'class-validator-jsonschema/build/options' +import type { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata' +import type { ReferenceObject, SchemaObject } from '../interfaces' +import { getGenerator } from './custom_generators' export const additionalSwaggerConverters = { - [ValidationTypes.NESTED_VALIDATION]: (): SchemaObject | void => undefined, - [ValidationTypes.IS_DEFINED]: (): SchemaObject | void => undefined, - [ValidationTypes.CUSTOM_VALIDATION]: ( - meta: ValidationMetadata, - options: IOptions, - ): SchemaObject | ReferenceObject | void => { - try { - return getGenerator(>meta.constraintCls)( - meta, - options, - ); - } catch { - if (meta.type === IS_INSTANCE) { - return meta.constraints[0].name === "Object" - ? { type: "object" } - : { - $ref: `${options.refPointerPrefix}${( - meta.constraints[0].name - )}`, - }; - } - } - }, -}; + [ValidationTypes.NESTED_VALIDATION]: (): SchemaObject | undefined => + undefined, + [ValidationTypes.IS_DEFINED]: (): SchemaObject | undefined => undefined, + [ValidationTypes.CUSTOM_VALIDATION]: ( + meta: ValidationMetadata, + options: IOptions, + ): SchemaObject | ReferenceObject | undefined => { + try { + return getGenerator(>meta.constraintCls)( + meta, + options, + ) + } catch { + if (meta.type === IS_INSTANCE) { + return meta.constraints[0].name === 'Object' + ? { type: 'object' } + : { + $ref: `${options.refPointerPrefix}${( + meta.constraints[0].name + )}`, + } + } + } + }, +} diff --git a/src/lib/asyncapi/class_validator/custom_generators.ts b/src/lib/asyncapi/class_validator/custom_generators.ts index a788d23..744a144 100644 --- a/src/lib/asyncapi/class_validator/custom_generators.ts +++ b/src/lib/asyncapi/class_validator/custom_generators.ts @@ -1,36 +1,36 @@ -import { ClassConstructor } from "class-transformer"; -import { IOptions } from "class-validator-jsonschema/build/options"; -import { ValidationMetadata } from "class-validator/types/metadata/ValidationMetadata"; -import { SchemaObject } from "../interfaces"; +import type { ClassConstructor } from 'class-transformer' +import type { IOptions } from 'class-validator-jsonschema/build/options' +import type { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata' +import type { SchemaObject } from '../interfaces' export type CustomSwaggerGenerator = ( - meta: ValidationMetadata, - options: IOptions, -) => void | SchemaObject; + meta: ValidationMetadata, + options: IOptions, +) => undefined | SchemaObject const customGenerators: Map< - ClassConstructor, - CustomSwaggerGenerator -> = new Map(); + ClassConstructor, + CustomSwaggerGenerator +> = new Map() export function registerGenerator( - validator: ClassConstructor, - generator: CustomSwaggerGenerator, + validator: ClassConstructor, + generator: CustomSwaggerGenerator, ): void { - customGenerators.set(validator, generator); + customGenerators.set(validator, generator) } export function getGenerator( - validator: ClassConstructor, + validator: ClassConstructor, ): CustomSwaggerGenerator { - const generator: CustomSwaggerGenerator | undefined = - customGenerators.get(validator); + const generator: CustomSwaggerGenerator | undefined = + customGenerators.get(validator) - if (!generator) { - throw new Error(); - } + if (!generator) { + throw new Error() + } - return generator; + return generator } // register custom: diff --git a/src/lib/asyncapi/class_validator/index.ts b/src/lib/asyncapi/class_validator/index.ts index f177966..8582ad6 100644 --- a/src/lib/asyncapi/class_validator/index.ts +++ b/src/lib/asyncapi/class_validator/index.ts @@ -1,140 +1,142 @@ -import { defaultMetadataStorage as classTransformerMetadataStorage } from "class-transformer/cjs/storage"; -import { ValidationTypes, getMetadataStorage } from "class-validator"; -import { validationMetadatasToSchemas as origValidationMetadatasToSchemas } from "class-validator-jsonschema"; -import { IOptions as JsonSchemaOptionsI } from "class-validator-jsonschema/build/options"; -import { ValidationMetadata } from "class-validator/types/metadata/ValidationMetadata"; -import _ from "lodash"; -import { - PublicMetadataStorage, - ReferenceObject, - SchemaObject, - SchemaObjects, -} from "../interfaces"; -import { additionalSwaggerConverters } from "./converters"; - -export type GeneratJsonSchemaOpts = Partial; +import { defaultMetadataStorage as classTransformerMetadataStorage } from 'class-transformer/cjs/storage' +import { ValidationTypes, getMetadataStorage } from 'class-validator' +import { validationMetadatasToSchemas as origValidationMetadatasToSchemas } from 'class-validator-jsonschema' +import type { IOptions as JsonSchemaOptionsI } from 'class-validator-jsonschema/build/options' +import type { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata' +import _ from 'lodash' +import type { + PublicMetadataStorage, + ReferenceObject, + SchemaObject, + SchemaObjects, +} from '../interfaces' +import { additionalSwaggerConverters } from './converters' + +export type GeneratJsonSchemaOpts = Partial export interface AsyncapiClassValidatorI { - SCHEMA_POINTER_PREFIX: string; - generateOptions?: GeneratJsonSchemaOpts; - validationMetadatas?: ValidationMetadata[]; + SCHEMA_POINTER_PREFIX: string + generateOptions?: GeneratJsonSchemaOpts + validationMetadatas?: ValidationMetadata[] } export class AsyncapiClassValidator { - private SCHEMA_POINTER_PREFIX: string; - private generateOptions: GeneratJsonSchemaOpts; - private validationMetadatas: ValidationMetadata[]; - - constructor({ - SCHEMA_POINTER_PREFIX, - generateOptions, - validationMetadatas, - }: AsyncapiClassValidatorI) { - this.SCHEMA_POINTER_PREFIX = SCHEMA_POINTER_PREFIX; - this.generateOptions = generateOptions ?? {}; - this.validationMetadatas = - validationMetadatas ?? - ((getMetadataStorage())) - .validationMetadatas; - } - - public generateJsonSchema(): SchemaObjects { - const schemaOptions: Partial = { - refPointerPrefix: this.SCHEMA_POINTER_PREFIX, - classTransformerMetadataStorage, - additionalConverters: additionalSwaggerConverters, - ...this.generateOptions, - }; - - return this.validationMetadatasToSchemas(schemaOptions); - } - - private validationMetadatasToSchemas( - userOptions?: Partial, - ): SchemaObjects { - const schemas: SchemaObjects = - origValidationMetadatasToSchemas(userOptions); - - this.updateRequired(schemas); - this.fixNullableRefs(schemas); - - return schemas; - } - - public isPropertyRequired(metas: ValidationMetadata[]): boolean { - return ( - metas.findIndex( - (m: ValidationMetadata) => - m.type === ValidationTypes.CONDITIONAL_VALIDATION, - ) < 0 - ); - } - - private updateRequired(schemas: SchemaObjects): void { - const metadatas = this.validationMetadatas; - - _(metadatas) - .groupBy("target.name") - .forEach((ownMetas: ValidationMetadata[], targetName: string) => { - const schema: SchemaObject = schemas[targetName]; - - const target = ownMetas[0].target; - const metas = [ - ...ownMetas, - ...this.getInheritedMetadatas(target, metadatas), - ]; - - schema.required = _(metas) - .groupBy("propertyName") - .filter((meta) => this.isPropertyRequired(meta)) - .map((_propMetas: ValidationMetadata[]) => _propMetas[0].propertyName) - .value(); - - if (schema.required.length === 0) { - delete schema.required; - } - }); - } - - private fixNullableRefs(schemas: SchemaObjects): void { - _(schemas) - .map("properties") - .map(_.values.bind(null)) - .flatten() - .forEach((prop: SchemaObject & { nullable?: boolean }) => { - if ( - (<{ $ref?: unknown }>prop).$ref !== undefined && - prop.nullable === true - ) { - this.fixNullableReferenceObject(prop); - } - }); - } - - private fixNullableReferenceObject( - prop: SchemaObject & { nullable?: boolean }, - ): void { - // nullable ref. by v3.1.0 spec - const ref = { $ref: ((prop)).$ref }; - delete prop.nullable; - prop.anyOf = [ref, { type: "null" }]; - - delete (<{ $ref?: unknown }>prop).$ref; - } - - private getInheritedMetadatas( - target: Function, - metadatas: ValidationMetadata[], - ): ValidationMetadata[] { - return metadatas.filter( - (d) => - d.target instanceof Function && - target.prototype instanceof d.target && - !_.find(metadatas, { - propertyName: d.propertyName, - target, - type: d.type, - }), - ); - } + private SCHEMA_POINTER_PREFIX: string + private generateOptions: GeneratJsonSchemaOpts + private validationMetadatas: ValidationMetadata[] + + constructor({ + SCHEMA_POINTER_PREFIX, + generateOptions, + validationMetadatas, + }: AsyncapiClassValidatorI) { + this.SCHEMA_POINTER_PREFIX = SCHEMA_POINTER_PREFIX + this.generateOptions = generateOptions ?? {} + this.validationMetadatas = + validationMetadatas ?? + ((getMetadataStorage())) + .validationMetadatas + } + + public generateJsonSchema(): SchemaObjects { + const schemaOptions: Partial = { + refPointerPrefix: this.SCHEMA_POINTER_PREFIX, + classTransformerMetadataStorage, + additionalConverters: additionalSwaggerConverters, + ...this.generateOptions, + } + + return this.validationMetadatasToSchemas(schemaOptions) + } + + private validationMetadatasToSchemas( + userOptions?: Partial, + ): SchemaObjects { + const schemas: SchemaObjects = origValidationMetadatasToSchemas(userOptions) + + this.updateRequired(schemas) + this.fixNullableRefs(schemas) + + return schemas + } + + public isPropertyRequired(metas: ValidationMetadata[]): boolean { + return ( + metas.findIndex( + (m: ValidationMetadata) => + m.type === ValidationTypes.CONDITIONAL_VALIDATION, + ) < 0 + ) + } + + private updateRequired(schemas: SchemaObjects): void { + const metadatas = this.validationMetadatas + + _(metadatas) + .groupBy('target.name') + .forEach((ownMetas: ValidationMetadata[], targetName: string) => { + const schema: SchemaObject = schemas[targetName] + + const target = ownMetas[0].target + const metas = [ + ...ownMetas, + ...this.getInheritedMetadatas(target, metadatas), + ] + + schema.required = _(metas) + .groupBy('propertyName') + .filter((meta) => this.isPropertyRequired(meta)) + .map((_propMetas: ValidationMetadata[]) => _propMetas[0].propertyName) + .value() + + if (schema.required.length === 0) { + // biome-ignore lint/performance/noDelete: + delete schema.required + } + }) + } + + private fixNullableRefs(schemas: SchemaObjects): void { + // biome-ignore lint/complexity/noForEach: + _(schemas) + .map('properties') + .map(_.values.bind(null)) + .flatten() + .forEach((prop: SchemaObject & { nullable?: boolean }) => { + if ( + (<{ $ref?: unknown }>prop).$ref !== undefined && + prop.nullable === true + ) { + this.fixNullableReferenceObject(prop) + } + }) + } + + private fixNullableReferenceObject( + prop: SchemaObject & { nullable?: boolean }, + ): void { + // nullable ref. by v3.1.0 spec + const ref = { $ref: ((prop)).$ref } + // biome-ignore lint/performance/noDelete: + delete prop.nullable + prop.anyOf = [ref, { type: 'null' }] + // biome-ignore lint/performance/noDelete: + delete (<{ $ref?: unknown }>prop).$ref + } + + private getInheritedMetadatas( + target: Function, + metadatas: ValidationMetadata[], + ): ValidationMetadata[] { + return metadatas.filter( + (d) => + d.target instanceof Function && + target.prototype instanceof d.target && + !_.find(metadatas, { + propertyName: d.propertyName, + target, + type: d.type, + }), + ) + } } diff --git a/src/lib/asyncapi/document_builder.ts b/src/lib/asyncapi/document_builder.ts index 518e955..6c9e2da 100644 --- a/src/lib/asyncapi/document_builder.ts +++ b/src/lib/asyncapi/document_builder.ts @@ -1,125 +1,125 @@ -import { cloneDeep, isUndefined, negate, pickBy } from "lodash"; -import { - AsyncAPIObject, - AsyncComponentsObject, - AsyncServerObject, - AsyncTagObject, - ExternalDocumentationObject, - SecuritySchemeObject, - TagObject, -} from "./interfaces"; - -interface IrisAsyncAPIObject extends Omit { - servers: Record; - components: AsyncComponentsObject; - tags: AsyncTagObject[]; +import { cloneDeep, isUndefined, negate, pickBy } from 'lodash' +import type { + AsyncAPIObject, + AsyncComponentsObject, + AsyncServerObject, + AsyncTagObject, + ExternalDocumentationObject, + SecuritySchemeObject, + TagObject, +} from './interfaces' + +interface IrisAsyncAPIObject extends Omit { + servers: Record + components: AsyncComponentsObject + tags: AsyncTagObject[] } +const buildDocumentBase = (): IrisAsyncAPIObject => ({ + asyncapi: '2.6.0', + info: { + title: '', + description: '', + version: '1.0.0', + contact: {}, + }, + channels: {}, + tags: [], + servers: {}, + components: {}, +}) + export class DocumentBuilder { - private readonly document: IrisAsyncAPIObject = buildDocumentBase(); + private readonly document: IrisAsyncAPIObject = buildDocumentBase() - public setTitle(title: string): this { - this.document.info.title = title; + public setTitle(title: string): this { + this.document.info.title = title - return this; - } + return this + } - public setId(id: string): this { - this.document.id = id; + public setId(id: string): this { + this.document.id = id - return this; - } + return this + } - public setDescription(description: string): this { - this.document.info.description = description; + public setDescription(description: string): this { + this.document.info.description = description - return this; - } + return this + } - public setVersion(version: string): this { - this.document.info.version = version; + public setVersion(version: string): this { + this.document.info.version = version - return this; - } + return this + } - public setTermsOfService(termsOfService: string): this { - this.document.info.termsOfService = termsOfService; + public setTermsOfService(termsOfService: string): this { + this.document.info.termsOfService = termsOfService - return this; - } + return this + } - public setContact(name: string, url: string, email: string): this { - this.document.info.contact = { name, url, email }; + public setContact(name: string, url: string, email: string): this { + this.document.info.contact = { name, url, email } - return this; - } + return this + } - public setLicense(name: string, url: string): this { - this.document.info.license = { name, url }; + public setLicense(name: string, url: string): this { + this.document.info.license = { name, url } - return this; - } + return this + } - public addServer( - key: string, - url: string, - protocol: string, - serverInfo: Omit = {}, - ): this { - this.document.servers[key] = { url, protocol, ...serverInfo }; + public addServer( + key: string, + url: string, + protocol: string, + serverInfo: Omit = {}, + ): this { + this.document.servers[key] = { url, protocol, ...serverInfo } - return this; - } + return this + } - public setExternalDoc(description: string, url: string): this { - this.document.externalDocs = { description, url }; + public setExternalDoc(description: string, url: string): this { + this.document.externalDocs = { description, url } - return this; - } + return this + } - public addTag( - name: string, - description = "", - externalDocs?: ExternalDocumentationObject, - ): this { - const tag = (pickBy( - { - name, - description, - externalDocs, - }, - negate(isUndefined), - )); + public addTag( + name: string, + description = '', + externalDocs?: ExternalDocumentationObject, + ): this { + const tag = (pickBy( + { + name, + description, + externalDocs, + }, + negate(isUndefined), + )) - this.document.tags = [...this.document.tags, tag]; + this.document.tags = [...this.document.tags, tag] - return this; - } + return this + } - public addSecurity(name: string, options: SecuritySchemeObject): this { - this.document.components.securitySchemes = { - ...(this.document.components.securitySchemes ?? {}), - [name]: options, - }; + public addSecurity(name: string, options: SecuritySchemeObject): this { + this.document.components.securitySchemes = { + ...(this.document.components.securitySchemes ?? {}), + [name]: options, + } - return this; - } + return this + } - public build(): Omit { - return cloneDeep(this.document); - } + public build(): Omit { + return cloneDeep(this.document) + } } - -const buildDocumentBase = (): IrisAsyncAPIObject => ({ - asyncapi: "2.2.0", - info: { - title: "", - description: "", - version: "1.0.0", - contact: {}, - }, - channels: {}, - tags: [], - servers: {}, - components: {}, -}); diff --git a/src/lib/asyncapi/index.ts b/src/lib/asyncapi/index.ts index f7d7376..f2e074c 100644 --- a/src/lib/asyncapi/index.ts +++ b/src/lib/asyncapi/index.ts @@ -1,6 +1,6 @@ -export * as interfaces from "./interfaces"; -export * from "./document_builder"; -export * from "./schema"; -export * from "./class_validator"; -export * from "./class_validator/custom_generators"; -export * as channels from "./schema/channels"; +export * as interfaces from './interfaces' +export * from './document_builder' +export * from './schema' +export * from './class_validator' +export * from './class_validator/custom_generators' +export * as channels from './schema/channels' diff --git a/src/lib/asyncapi/interfaces.ts b/src/lib/asyncapi/interfaces.ts index c796709..3b5cf59 100644 --- a/src/lib/asyncapi/interfaces.ts +++ b/src/lib/asyncapi/interfaces.ts @@ -1,204 +1,204 @@ -import { ValidationMetadata } from "class-validator/types/metadata/ValidationMetadata"; -import { - InfoObject, - OAuthFlowsObject, - ReferenceObject, - SchemaObject, - SecuritySchemeType, - ServerObject, - ServerVariableObject, -} from "./interfaces_openapi"; - -export * from "./interfaces_openapi"; - -export type SchemaObjects = Record; +import type { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata' +import type { + InfoObject, + OAuthFlowsObject, + ReferenceObject, + SchemaObject, + SecuritySchemeType, + ServerObject, + ServerVariableObject, +} from './interfaces_openapi' + +export * from './interfaces_openapi' + +export type SchemaObjects = Record export interface PublicMetadataStorage { - validationMetadatas: ValidationMetadata[]; + validationMetadatas: ValidationMetadata[] } export enum Operation { - subscribe = "subscribe", - publish = "publish", + subscribe = 'subscribe', + publish = 'publish', } export enum ChannelIs { - queue = "queue", - routingKey = "routingKey", + queue = 'queue', + routingKey = 'routingKey', } export interface AsyncAPIObject { - asyncapi: string; - id?: string; - info: InfoObject; - servers?: Record; - channels: AsyncChannelsObject; - components?: AsyncComponentsObject; - tags?: AsyncTagObject[]; - externalDocs?: ExternalDocumentationObject; - defaultContentType?: string; + asyncapi: string + id?: string + info: InfoObject + servers?: Record + channels: AsyncChannelsObject + components?: AsyncComponentsObject + tags?: AsyncTagObject[] + externalDocs?: ExternalDocumentationObject + defaultContentType?: string } export interface AsyncComponentsObject { - schemas?: Record; - messages?: Record; - securitySchemes?: Record; - parameters?: Record; - correlationIds?: Record; - operationTraits?: Record; - messageTraits?: Record; - serverBindings?: Record; - channelBindings?: Record; - operationBindings?: Record; - messageBindings?: Record; + schemas?: Record + messages?: Record + securitySchemes?: Record + parameters?: Record + correlationIds?: Record + operationTraits?: Record + messageTraits?: Record + serverBindings?: Record + channelBindings?: Record + operationBindings?: Record + messageBindings?: Record } export interface AsyncServerVariableObject extends ServerVariableObject { - examples?: string[]; + examples?: string[] } -export type SecurityObject = Record; +export type SecurityObject = Record -export type AmqpServerBindingObject = {}; +export type AmqpServerBindingObject = {} -export interface AsyncServerObject extends Omit { - variables?: Record; - protocol: string; - protocolVersion?: string; - security?: SecurityObject[]; - bindings?: Record; +export interface AsyncServerObject extends Omit { + variables?: Record + protocol: string + protocolVersion?: string + security?: SecurityObject[] + bindings?: Record } -export type AsyncChannelsObject = Record; +export type AsyncChannelsObject = Record export interface AsyncChannelObject { - description?: string; - subscribe?: AsyncOperationObject; - publish?: AsyncOperationObject; - parameters?: Record; - bindings?: Record; + description?: string + subscribe?: AsyncOperationObject + publish?: AsyncOperationObject + parameters?: Record + bindings?: Record } export interface AsyncOperationObject { - operationId?: string; - summary?: string; - description?: string; - tags?: AsyncTagObject[]; - externalDocs?: ExternalDocumentationObject; - bindings?: Record; - traits?: Record; - message?: AsyncMessageObject | ReferenceObject; + operationId?: string + summary?: string + description?: string + tags?: AsyncTagObject[] + externalDocs?: ExternalDocumentationObject + bindings?: Record + traits?: Record + message?: AsyncMessageObject | ReferenceObject } export interface AsyncOperationTraitObject { - operationId?: string; - summary?: string; - description?: string; - tags?: AsyncTagObject[]; - externalDocs?: ExternalDocumentationObject; - bindings?: Record; + operationId?: string + summary?: string + description?: string + tags?: AsyncTagObject[] + externalDocs?: ExternalDocumentationObject + bindings?: Record } export interface AsyncMessageTraitObject { - headers?: SchemaObject; - correlationId?: AsyncCorrelationObject; - schemaFormat?: string; - contentType?: string; - name?: string; - title?: string; - summary?: string; - description?: string; - tags?: AsyncTagObject[]; - externalDocs?: ExternalDocumentationObject; - bindings?: Record; + headers?: SchemaObject + correlationId?: AsyncCorrelationObject + schemaFormat?: string + contentType?: string + name?: string + title?: string + summary?: string + description?: string + tags?: AsyncTagObject[] + externalDocs?: ExternalDocumentationObject + bindings?: Record } export interface AsyncCorrelationObject { - description?: string; - location: string; + description?: string + location: string } export interface AsyncMessageObject extends AsyncMessageTraitObject { - payload?: unknown; - traits?: AsyncMessageTraitObject; + payload?: unknown + traits?: AsyncMessageTraitObject } -export type ParameterObject = BaseParameterObject; +export type ParameterObject = BaseParameterObject export interface BaseParameterObject { - description?: string; - schema?: SchemaObject | ReferenceObject; - location?: string; + description?: string + schema?: SchemaObject | ReferenceObject + location?: string } export interface AmqpQueue { - name: string; - durable?: boolean; - exclusive?: boolean; - autoDelete?: boolean; - vhost?: string; + name: string + durable?: boolean + exclusive?: boolean + autoDelete?: boolean + vhost?: string } export interface AmqpExchange { - name: string; - type: string; - durable?: boolean; - autoDelete?: boolean; - vhost?: string; + name: string + type: string + durable?: boolean + autoDelete?: boolean + vhost?: string } export interface AmqpChannelBindingObject { - is: ChannelIs; - exchange?: AmqpExchange; - queue?: AmqpQueue; - bindingVersion?: string; + is: ChannelIs + exchange?: AmqpExchange + queue?: AmqpQueue + bindingVersion?: string } export interface AmqpChannelBindingExchange extends AmqpChannelBindingObject { - is: ChannelIs.routingKey; - exchange: AmqpExchange; + is: ChannelIs.routingKey + exchange: AmqpExchange } export interface AmqpChannelBindingQueue extends AmqpChannelBindingObject { - is: ChannelIs.queue; - queue: AmqpQueue; + is: ChannelIs.queue + queue: AmqpQueue } export interface AmqpOperationBindingObject { - expiration?: number; - userId?: string; - cc?: string[]; - priority?: number; - deliveryMode?: number; - mandatory?: boolean; - bcc?: string[]; - replyTo?: string; - timestamp?: boolean; - ack?: boolean; - bindingVersion?: string; + expiration?: number + userId?: string + cc?: string[] + priority?: number + deliveryMode?: number + mandatory?: boolean + bcc?: string[] + replyTo?: string + timestamp?: boolean + ack?: boolean + bindingVersion?: string } export interface AmqpMessageBindingObject { - contentEncoding?: string; - messageType?: string; - bindingVersion?: string; + contentEncoding?: string + messageType?: string + bindingVersion?: string } export interface AsyncTagObject { - name: string; - description?: string; - externalDocs?: ExternalDocumentationObject; + name: string + description?: string + externalDocs?: ExternalDocumentationObject } export interface AsyncSecuritySchemeObject { - type: SecuritySchemeType; - description?: string; - name?: string; - in?: string; - scheme?: string; - bearerFormat?: string; - flows?: OAuthFlowsObject; - openIdConnectUrl?: string; + type: SecuritySchemeType + description?: string + name?: string + in?: string + scheme?: string + bearerFormat?: string + flows?: OAuthFlowsObject + openIdConnectUrl?: string } export interface ExternalDocumentationObject { - description?: string; - url: string; + description?: string + url: string } diff --git a/src/lib/asyncapi/interfaces_openapi.ts b/src/lib/asyncapi/interfaces_openapi.ts index b174276..6c50df4 100644 --- a/src/lib/asyncapi/interfaces_openapi.ts +++ b/src/lib/asyncapi/interfaces_openapi.ts @@ -1,236 +1,235 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ /** * Taken from @NestJS/swagger https://github.com/nestjs/swagger */ -import { SchemaObject as SchemaObjectOpenapi } from "openapi3-ts"; +import type { SchemaObject as SchemaObjectOpenapi } from 'openapi3-ts' export interface OpenAPIObject { - openapi: string; - info: InfoObject; - servers?: ServerObject[]; - paths: PathsObject; - components?: ComponentsObject; - security?: SecurityRequirementObject[]; - tags?: TagObject[]; - externalDocs?: ExternalDocumentationObject; + openapi: string + info: InfoObject + servers?: ServerObject[] + paths: PathsObject + components?: ComponentsObject + security?: SecurityRequirementObject[] + tags?: TagObject[] + externalDocs?: ExternalDocumentationObject } export interface InfoObject { - title: string; - description?: string; - termsOfService?: string; - contact?: ContactObject; - license?: LicenseObject; - version: string; + title: string + description?: string + termsOfService?: string + contact?: ContactObject + license?: LicenseObject + version: string } export interface ContactObject { - name?: string; - url?: string; - email?: string; + name?: string + url?: string + email?: string } export interface LicenseObject { - name: string; - url?: string; + name: string + url?: string } export interface ServerObject { - url: string; - description?: string; - variables?: Record; + url: string + description?: string + variables?: Record } export interface ServerVariableObject { - enum?: string[] | boolean[] | number[]; - default: string | boolean | number; - description?: string; + enum?: string[] | boolean[] | number[] + default: string | boolean | number + description?: string } export interface ComponentsObject { - schemas?: Record; - responses?: Record; - parameters?: Record; - examples?: Record; - requestBodies?: Record; - headers?: Record; - securitySchemes?: Record; - links?: Record; - callbacks?: Record; -} - -export type PathsObject = Record; + schemas?: Record + responses?: Record + parameters?: Record + examples?: Record + requestBodies?: Record + headers?: Record + securitySchemes?: Record + links?: Record + callbacks?: Record +} + +export type PathsObject = Record export interface PathItemObject { - $ref?: string; - summary?: string; - description?: string; - get?: OperationObject; - put?: OperationObject; - post?: OperationObject; - delete?: OperationObject; - options?: OperationObject; - head?: OperationObject; - patch?: OperationObject; - trace?: OperationObject; - servers?: ServerObject[]; - parameters?: (ParameterObject | ReferenceObject)[]; + $ref?: string + summary?: string + description?: string + get?: OperationObject + put?: OperationObject + post?: OperationObject + delete?: OperationObject + options?: OperationObject + head?: OperationObject + patch?: OperationObject + trace?: OperationObject + servers?: ServerObject[] + parameters?: (ParameterObject | ReferenceObject)[] } export interface OperationObject { - tags?: string[]; - summary?: string; - description?: string; - externalDocs?: ExternalDocumentationObject; - operationId?: string; - parameters?: (ParameterObject | ReferenceObject)[]; - requestBody?: RequestBodyObject | ReferenceObject; - responses: ResponsesObject; - callbacks?: CallbacksObject; - deprecated?: boolean; - security?: SecurityRequirementObject[]; - servers?: ServerObject[]; + tags?: string[] + summary?: string + description?: string + externalDocs?: ExternalDocumentationObject + operationId?: string + parameters?: (ParameterObject | ReferenceObject)[] + requestBody?: RequestBodyObject | ReferenceObject + responses: ResponsesObject + callbacks?: CallbacksObject + deprecated?: boolean + security?: SecurityRequirementObject[] + servers?: ServerObject[] } export interface ExternalDocumentationObject { - description?: string; - url: string; + description?: string + url: string } -export type ParameterLocation = "query" | "header" | "path" | "cookie"; +export type ParameterLocation = 'query' | 'header' | 'path' | 'cookie' export type ParameterStyle = - | "matrix" - | "label" - | "form" - | "simple" - | "spaceDelimited" - | "pipeDelimited" - | "deepObject"; + | 'matrix' + | 'label' + | 'form' + | 'simple' + | 'spaceDelimited' + | 'pipeDelimited' + | 'deepObject' export interface BaseParameterObject { - description?: string; - required?: boolean; - deprecated?: boolean; - allowEmptyValue?: boolean; - style?: ParameterStyle; - explode?: boolean; - allowReserved?: boolean; - schema?: SchemaObject | ReferenceObject; - examples?: Record; - example?: any; - content?: ContentObject; + description?: string + required?: boolean + deprecated?: boolean + allowEmptyValue?: boolean + style?: ParameterStyle + explode?: boolean + allowReserved?: boolean + schema?: SchemaObject | ReferenceObject + examples?: Record + example?: any + content?: ContentObject } export interface ParameterObject extends BaseParameterObject { - name: string; - in: ParameterLocation; + name: string + in: ParameterLocation } export interface RequestBodyObject { - description?: string; - content: ContentObject; - required?: boolean; + description?: string + content: ContentObject + required?: boolean } -export type ContentObject = Record; +export type ContentObject = Record export interface MediaTypeObject { - schema?: SchemaObject | ReferenceObject; - examples?: ExamplesObject; - example?: any; - encoding?: EncodingObject; + schema?: SchemaObject | ReferenceObject + examples?: ExamplesObject + example?: any + encoding?: EncodingObject } -export type EncodingObject = Record; +export type EncodingObject = Record export interface EncodingPropertyObject { - contentType?: string; - headers?: Record; - style?: string; - explode?: boolean; - allowReserved?: boolean; + contentType?: string + headers?: Record + style?: string + explode?: boolean + allowReserved?: boolean } export interface ResponsesObject - extends Record { - default?: ResponseObject | ReferenceObject; + extends Record { + default?: ResponseObject | ReferenceObject } export interface ResponseObject { - description: string; - headers?: HeadersObject; - content?: ContentObject; - links?: LinksObject; + description: string + headers?: HeadersObject + content?: ContentObject + links?: LinksObject } -export type CallbacksObject = Record; -export type CallbackObject = Record; -export type HeadersObject = Record; +export type CallbacksObject = Record +export type CallbackObject = Record +export type HeadersObject = Record export interface ExampleObject { - summary?: string; - description?: string; - value?: any; - externalValue?: string; + summary?: string + description?: string + value?: any + externalValue?: string } -export type LinksObject = Record; +export type LinksObject = Record export interface LinkObject { - operationRef?: string; - operationId?: string; - parameters?: LinkParametersObject; - requestBody?: any | string; - description?: string; - server?: ServerObject; + operationRef?: string + operationId?: string + parameters?: LinkParametersObject + requestBody?: any | string + description?: string + server?: ServerObject } -export type LinkParametersObject = Record; -export type HeaderObject = BaseParameterObject; +export type LinkParametersObject = Record +export type HeaderObject = BaseParameterObject export interface TagObject { - name: string; - description?: string; - externalDocs?: ExternalDocumentationObject; + name: string + description?: string + externalDocs?: ExternalDocumentationObject } -export type ExamplesObject = Record; +export type ExamplesObject = Record export interface ReferenceObject { - $ref: string; + $ref: string } -export type SchemaObject = SchemaObjectOpenapi; +export type SchemaObject = SchemaObjectOpenapi -export type SchemasObject = Record; +export type SchemasObject = Record export interface DiscriminatorObject { - propertyName: string; - mapping?: Record; + propertyName: string + mapping?: Record } -export type SecuritySchemeType = "apiKey" | "http" | "oauth2" | "openIdConnect"; +export type SecuritySchemeType = 'apiKey' | 'http' | 'oauth2' | 'openIdConnect' export interface SecuritySchemeObject { - type: SecuritySchemeType; - description?: string; - name?: string; - in?: string; - scheme?: string; - bearerFormat?: string; - flows?: OAuthFlowsObject; - openIdConnectUrl?: string; + type: SecuritySchemeType + description?: string + name?: string + in?: string + scheme?: string + bearerFormat?: string + flows?: OAuthFlowsObject + openIdConnectUrl?: string } export interface OAuthFlowsObject { - implicit?: OAuthFlowObject; - password?: OAuthFlowObject; - clientCredentials?: OAuthFlowObject; - authorizationCode?: OAuthFlowObject; + implicit?: OAuthFlowObject + password?: OAuthFlowObject + clientCredentials?: OAuthFlowObject + authorizationCode?: OAuthFlowObject } export interface OAuthFlowObject { - authorizationUrl?: string; - tokenUrl?: string; - refreshUrl?: string; - scopes: ScopesObject; + authorizationUrl?: string + tokenUrl?: string + refreshUrl?: string + scopes: ScopesObject } -export type ScopesObject = Record; -export type SecurityRequirementObject = Record; +export type ScopesObject = Record +export type SecurityRequirementObject = Record diff --git a/src/lib/asyncapi/schema/channels.ts b/src/lib/asyncapi/schema/channels.ts index abc33ea..6971bb3 100644 --- a/src/lib/asyncapi/schema/channels.ts +++ b/src/lib/asyncapi/schema/channels.ts @@ -1,265 +1,265 @@ -import { ProcessedMessageMetadataI } from "../../message.interfaces"; -import { ProcessedMessageHandlerMetadataI } from "../../message_handler.interfaces"; -import * as interfaces from "../interfaces"; - -type MessageHeadersWithPropsI = Omit & { - properties: Record< - string, - interfaces.SchemaObject | interfaces.ReferenceObject - >; -}; +import type { ProcessedMessageMetadataI } from '../../message.interfaces' +import type { ProcessedMessageHandlerMetadataI } from '../../message_handler.interfaces' +import * as interfaces from '../interfaces' + +type MessageHeadersWithPropsI = Omit & { + properties: Record< + string, + interfaces.SchemaObject | interfaces.ReferenceObject + > +} export type CustomChannelClassesI = { - subscriptionChannelClass?: typeof SubscriptionChannel; - publishChannelClass?: typeof PublishChannel; -}; + subscriptionChannelClass?: typeof SubscriptionChannel + publishChannelClass?: typeof PublishChannel +} abstract class Channel { - protected message: ProcessedMessageMetadataI; - protected schemaPointerPrefix: string; - abstract readonly operation: interfaces.Operation; - protected readonly vhost: string = "/"; - - constructor(schemaPointerPrefix: string, message: ProcessedMessageMetadataI) { - this.message = message; - this.schemaPointerPrefix = schemaPointerPrefix; - } - - public getChannelKey(): string { - const { exchangeName, routingKey } = this.message.processedConfig; - const channelKey = `${exchangeName}/${routingKey}`; - - return channelKey; - } - - public asAsyncapiChannelsObject(): interfaces.AsyncChannelsObject { - return { [this.getChannelKey()]: this.asAsyncapiChannelObject() }; - } - - public asAsyncapiChannelObject(): interfaces.AsyncChannelObject { - return { - [this.operation]: { - message: this.message2Spec(), - }, - bindings: { - amqp: this.message2AmqpBindings(), - }, - }; - } - - protected message2Spec(): - | interfaces.AsyncMessageObject - | interfaces.ReferenceObject { - const { targetClassName } = this.message; - - return this.additionalMessageProperties({ - name: targetClassName, - title: targetClassName, - payload: { - $ref: `${this.schemaPointerPrefix}${targetClassName}`, - }, - }); - } - - protected message2AmqpBindings(): - | interfaces.AmqpChannelBindingExchange - | interfaces.AmqpChannelBindingQueue { - const { exchangeOptions, exchangeName, exchangeType } = - this.message.processedConfig; - - return { - is: interfaces.ChannelIs.routingKey, - exchange: { - ...exchangeOptions, - vhost: this.vhost, - name: exchangeName, - type: exchangeType, - }, - }; - } - - protected additionalMessageProperties( - intermediate: interfaces.AsyncMessageObject, - ): interfaces.AsyncMessageObject { - return { - headers: this.getMessageHeaders(), - ...intermediate, - ...this.getAdditionalMessageSchemaProperties(), - }; - } - - protected getMessageHeaders(): MessageHeadersWithPropsI { - const headerProps: Record = { - "x-scope": { - description: "Message scope. Default is INTERNAL", - type: "string", - ...this.getEnumIfNotEmpty(this.getCustomScopeHeaderValue()), - }, - "x-ttl": { - description: - "TTL of the message. If set to -1 (default) will use brokers default.", - type: "number", - ...this.getEnumIfNotEmpty(this.getCustomTTLHeaderValue()), - }, - "x-roles-allowed": { - description: "Allowed roles for this message. Default is empty", - type: "array", - ...this.getEnumIfNotEmpty(this.getCustomRolesHeaderValue()), - }, - "x-dead-letter": { - description: "Dead letter queue definition. Default is dead-letter", - type: "string", - ...this.getEnumIfNotEmpty(this.getCustomDeadLetterHeaderValue()), - }, - }; - - const headers: MessageHeadersWithPropsI = { - properties: headerProps, - type: "object", - }; - - return headers; - } - - protected getEnumIfNotEmpty(values: T[]): { enum?: T[] } { - if (values.length > 0) { - return { enum: values }; - } - - return {}; - } - - protected getCustomTTLHeaderValue(): number[] { - const { ttl } = this.message.processedConfig; - - return [ttl ?? -1]; - } - protected getCustomScopeHeaderValue(): string[] { - return [this.message.processedConfig.scope]; - } - - protected getCustomRolesHeaderValue(): string[] { - return []; - } - protected getCustomDeadLetterHeaderValue(): string[] { - const { deadLetter } = this.message.processedConfig; - - return deadLetter.length === 0 ? [] : [deadLetter]; - } - - protected getAdditionalMessageSchemaProperties(): Record< - string, - interfaces.SchemaObject | interfaces.ReferenceObject - > { - return {}; - } + protected message: ProcessedMessageMetadataI + protected schemaPointerPrefix: string + abstract readonly operation: interfaces.Operation + protected readonly vhost: string = '/' + + constructor(schemaPointerPrefix: string, message: ProcessedMessageMetadataI) { + this.message = message + this.schemaPointerPrefix = schemaPointerPrefix + } + + public getChannelKey(): string { + const { exchangeName, routingKey } = this.message.processedConfig + const channelKey = `${exchangeName}/${routingKey}` + + return channelKey + } + + public asAsyncapiChannelsObject(): interfaces.AsyncChannelsObject { + return { [this.getChannelKey()]: this.asAsyncapiChannelObject() } + } + + public asAsyncapiChannelObject(): interfaces.AsyncChannelObject { + return { + [this.operation]: { + message: this.message2Spec(), + }, + bindings: { + amqp: this.message2AmqpBindings(), + }, + } + } + + protected message2Spec(): + | interfaces.AsyncMessageObject + | interfaces.ReferenceObject { + const { targetClassName } = this.message + + return this.additionalMessageProperties({ + name: targetClassName, + title: targetClassName, + payload: { + $ref: `${this.schemaPointerPrefix}${targetClassName}`, + }, + }) + } + + protected message2AmqpBindings(): + | interfaces.AmqpChannelBindingExchange + | interfaces.AmqpChannelBindingQueue { + const { exchangeOptions, exchangeName, exchangeType } = + this.message.processedConfig + + return { + is: interfaces.ChannelIs.routingKey, + exchange: { + ...exchangeOptions, + vhost: this.vhost, + name: exchangeName, + type: exchangeType, + }, + } + } + + protected additionalMessageProperties( + intermediate: interfaces.AsyncMessageObject, + ): interfaces.AsyncMessageObject { + return { + headers: this.getMessageHeaders(), + ...intermediate, + ...this.getAdditionalMessageSchemaProperties(), + } + } + + protected getMessageHeaders(): MessageHeadersWithPropsI { + const headerProps: Record = { + 'x-scope': { + description: 'Message scope. Default is INTERNAL', + type: 'string', + ...this.getEnumIfNotEmpty(this.getCustomScopeHeaderValue()), + }, + 'x-ttl': { + description: + 'TTL of the message. If set to -1 (default) will use brokers default.', + type: 'number', + ...this.getEnumIfNotEmpty(this.getCustomTTLHeaderValue()), + }, + 'x-roles-allowed': { + description: 'Allowed roles for this message. Default is empty', + type: 'array', + ...this.getEnumIfNotEmpty(this.getCustomRolesHeaderValue()), + }, + 'x-dead-letter': { + description: 'Dead letter queue definition. Default is dead-letter', + type: 'string', + ...this.getEnumIfNotEmpty(this.getCustomDeadLetterHeaderValue()), + }, + } + + const headers: MessageHeadersWithPropsI = { + properties: headerProps, + type: 'object', + } + + return headers + } + + protected getEnumIfNotEmpty(values: T[]): { enum?: T[] } { + if (values.length > 0) { + return { enum: values } + } + + return {} + } + + protected getCustomTTLHeaderValue(): number[] { + const { ttl } = this.message.processedConfig + + return [ttl ?? -1] + } + protected getCustomScopeHeaderValue(): string[] { + return [this.message.processedConfig.scope] + } + + protected getCustomRolesHeaderValue(): string[] { + return [] + } + protected getCustomDeadLetterHeaderValue(): string[] { + const { deadLetter } = this.message.processedConfig + + return deadLetter.length === 0 ? [] : [deadLetter] + } + + protected getAdditionalMessageSchemaProperties(): Record< + string, + interfaces.SchemaObject | interfaces.ReferenceObject + > { + return {} + } } export class SubscriptionChannel extends Channel { - readonly operation: interfaces.Operation = interfaces.Operation.subscribe; + readonly operation: interfaces.Operation = interfaces.Operation.subscribe } export class PublishChannel extends Channel { - readonly operation: interfaces.Operation = interfaces.Operation.publish; - - protected handler: ProcessedMessageHandlerMetadataI; - - constructor( - schemaPointerPrefix: string, - message: ProcessedMessageMetadataI, - handler: ProcessedMessageHandlerMetadataI, - ) { - super(schemaPointerPrefix, message); - this.handler = handler; - } - - public getChannelKey(): string { - const { exchangeName } = this.message.processedConfig; - const { queueName } = this.handler.processedConfig; - const channelKey = `${exchangeName}/${queueName}`; - - return channelKey; - } - - protected message2AmqpBindings(): - | interfaces.AmqpChannelBindingExchange - | interfaces.AmqpChannelBindingQueue { - return { - ...super.message2AmqpBindings(), - queue: this.getQueueBinding(), - }; - } - - private getQueueBinding(): interfaces.AmqpQueue { - const { queueName, queueOptions } = this.handler.processedConfig; - - return { - ...queueOptions, - name: queueName, - vhost: this.vhost, - }; - } - - protected getAdditionalMessageSchemaProperties(): Record< - string, - interfaces.SchemaObject | interfaces.ReferenceObject - > { - const { kind } = this.handler; - - if (kind === "WITH_REPLY") { - return { - "x-response": { - $ref: `${this.schemaPointerPrefix}${this.handler.replyMessageClassName}`, - }, - }; - } - - return {}; - } + readonly operation: interfaces.Operation = interfaces.Operation.publish + + protected handler: ProcessedMessageHandlerMetadataI + + constructor( + schemaPointerPrefix: string, + message: ProcessedMessageMetadataI, + handler: ProcessedMessageHandlerMetadataI, + ) { + super(schemaPointerPrefix, message) + this.handler = handler + } + + public getChannelKey(): string { + const { exchangeName } = this.message.processedConfig + const { queueName } = this.handler.processedConfig + const channelKey = `${exchangeName}/${queueName}` + + return channelKey + } + + protected message2AmqpBindings(): + | interfaces.AmqpChannelBindingExchange + | interfaces.AmqpChannelBindingQueue { + return { + ...super.message2AmqpBindings(), + queue: this.getQueueBinding(), + } + } + + private getQueueBinding(): interfaces.AmqpQueue { + const { queueName, queueOptions } = this.handler.processedConfig + + return { + ...queueOptions, + name: queueName, + vhost: this.vhost, + } + } + + protected getAdditionalMessageSchemaProperties(): Record< + string, + interfaces.SchemaObject | interfaces.ReferenceObject + > { + const { kind } = this.handler + + if (kind === 'WITH_REPLY') { + return { + 'x-response': { + $ref: `${this.schemaPointerPrefix}${this.handler.replyMessageClassName}`, + }, + } + } + + return {} + } } // biome-ignore lint/complexity/noStaticOnlyClass: export class IrisChannels { - private static subscriptionChannelClass: typeof SubscriptionChannel = - SubscriptionChannel; - private static publishChannelClass: typeof PublishChannel = PublishChannel; - - public static generateChannels( - message: ProcessedMessageMetadataI, - messageHandlers: ProcessedMessageHandlerMetadataI[], - schemaPointerPrefix: string, - ): Channel | Channel[] { - const handlers = messageHandlers.filter( - (mh) => mh.messageClass === message.target, - ); - if (handlers.length === 0) { - return new IrisChannels.subscriptionChannelClass( - schemaPointerPrefix, - message, - ); - } - - return handlers.map( - (handler) => - new IrisChannels.publishChannelClass( - schemaPointerPrefix, - message, - handler, - ), - ); - } - - public static setCustomChannelClasses({ - publishChannelClass, - subscriptionChannelClass, - }: CustomChannelClassesI): IrisChannels { - if (publishChannelClass !== undefined) { - IrisChannels.publishChannelClass = publishChannelClass; - } - if (subscriptionChannelClass !== undefined) { - IrisChannels.subscriptionChannelClass = subscriptionChannelClass; - } - - return IrisChannels; - } + private static subscriptionChannelClass: typeof SubscriptionChannel = + SubscriptionChannel + private static publishChannelClass: typeof PublishChannel = PublishChannel + + public static generateChannels( + message: ProcessedMessageMetadataI, + messageHandlers: ProcessedMessageHandlerMetadataI[], + schemaPointerPrefix: string, + ): Channel | Channel[] { + const handlers = messageHandlers.filter( + (mh) => mh.messageClass === message.target, + ) + if (handlers.length === 0) { + return new IrisChannels.subscriptionChannelClass( + schemaPointerPrefix, + message, + ) + } + + return handlers.map( + (handler) => + new IrisChannels.publishChannelClass( + schemaPointerPrefix, + message, + handler, + ), + ) + } + + public static setCustomChannelClasses({ + publishChannelClass, + subscriptionChannelClass, + }: CustomChannelClassesI): IrisChannels { + if (publishChannelClass !== undefined) { + IrisChannels.publishChannelClass = publishChannelClass + } + if (subscriptionChannelClass !== undefined) { + IrisChannels.subscriptionChannelClass = subscriptionChannelClass + } + + return IrisChannels + } } diff --git a/src/lib/asyncapi/schema/index.ts b/src/lib/asyncapi/schema/index.ts index 10bb8f6..aa2d7e4 100644 --- a/src/lib/asyncapi/schema/index.ts +++ b/src/lib/asyncapi/schema/index.ts @@ -1,128 +1,127 @@ -import { Exclude } from "class-transformer"; -import { IsOptional, IsString } from "class-validator"; -import { JSONSchema } from "class-validator-jsonschema"; -import { ProcessedMessageMetadataI } from "../../message.interfaces"; -import { ProcessedMessageHandlerMetadataI } from "../../message_handler.interfaces"; +import { Exclude } from 'class-transformer' +import { IsOptional, IsString } from 'class-validator' +import { JSONSchema } from 'class-validator-jsonschema' +import type { ProcessedMessageMetadataI } from '../../message.interfaces' +import type { ProcessedMessageHandlerMetadataI } from '../../message_handler.interfaces' import { - ResourceMessage, - SnapshotRequested, - SubscribeInternal, -} from "../../subscription.messages"; + ResourceMessage, + SnapshotRequested, + SubscribeInternal, +} from '../../subscription.messages' import { - AsyncapiClassValidator, - AsyncapiClassValidatorI, -} from "../class_validator"; -import * as interfaces from "../interfaces"; -import { IrisChannels } from "./channels"; -import { IrisSchemas } from "./iris"; + AsyncapiClassValidator, + type AsyncapiClassValidatorI, +} from '../class_validator' +import type * as interfaces from '../interfaces' +import { IrisChannels } from './channels' +import { IrisSchemas } from './iris' const internallyDefinedMessages = [ - SubscribeInternal, - ResourceMessage, - SnapshotRequested, -]; + SubscribeInternal, + ResourceMessage, + SnapshotRequested, +] export type AsyncapiSchemaI = { - messages: ProcessedMessageMetadataI[]; - messageHandlers: ProcessedMessageHandlerMetadataI[]; - SCHEMA_POINTER_PREFIX: string; -} & Omit; + messages: ProcessedMessageMetadataI[] + messageHandlers: ProcessedMessageHandlerMetadataI[] + SCHEMA_POINTER_PREFIX: string +} & Omit export class AsyncapiSchema { - private messages: ProcessedMessageMetadataI[]; - private messageHandlers: ProcessedMessageHandlerMetadataI[]; - private irisSchemas: IrisSchemas; - private SCHEMA_POINTER_PREFIX: string; + private messages: ProcessedMessageMetadataI[] + private messageHandlers: ProcessedMessageHandlerMetadataI[] + private irisSchemas: IrisSchemas + private SCHEMA_POINTER_PREFIX: string - constructor({ - messages, - messageHandlers, - SCHEMA_POINTER_PREFIX, - ...classValidatorOpts - }: AsyncapiSchemaI) { - this.messages = messages; - this.messageHandlers = messageHandlers; - this.SCHEMA_POINTER_PREFIX = SCHEMA_POINTER_PREFIX; - const asyncapiClassValidator = new AsyncapiClassValidator({ - SCHEMA_POINTER_PREFIX, - ...classValidatorOpts, - }); - this.irisSchemas = new IrisSchemas({ - messages: messages, - asyncapiClassValidator, - }); + constructor({ + messages, + messageHandlers, + SCHEMA_POINTER_PREFIX, + ...classValidatorOpts + }: AsyncapiSchemaI) { + this.messages = messages + this.messageHandlers = messageHandlers + this.SCHEMA_POINTER_PREFIX = SCHEMA_POINTER_PREFIX + const asyncapiClassValidator = new AsyncapiClassValidator({ + SCHEMA_POINTER_PREFIX, + ...classValidatorOpts, + }) + this.irisSchemas = new IrisSchemas({ + messages: messages, + asyncapiClassValidator, + }) - this.additionallyDecorateMessages(); - } + this.additionallyDecorateMessages() + } - public getSchemas(): interfaces.SchemaObjects { - return this.irisSchemas.getSchemasForMessages(); - } + public getSchemas(): interfaces.SchemaObjects { + return this.irisSchemas.getSchemasForMessages() + } - public getChannels(): interfaces.AsyncChannelsObject { - return this.messages - .flatMap((message) => - IrisChannels.generateChannels( - message, - this.messageHandlers, - this.SCHEMA_POINTER_PREFIX, - ), - ) - .reduce( - (acc, channel) => ({ - ...acc, - ...channel.asAsyncapiChannelsObject(), - }), - {}, - ); - } + public getChannels(): interfaces.AsyncChannelsObject { + return this.messages + .flatMap((message) => + IrisChannels.generateChannels( + message, + this.messageHandlers, + this.SCHEMA_POINTER_PREFIX, + ), + ) + .reduce( + (acc, channel) => ({ + // biome-ignore lint/performance/noAccumulatingSpread: + ...acc, + ...channel.asAsyncapiChannelsObject(), + }), + {}, + ) + } - private additionallyDecorateMessages(): void { - for (const message of this.messages) { - this.assureMessageIsIncludedInSchema(message.target); - this.decorateMessageWithIrisAdditionalProperties( - message.target, - ); - } - } + private additionallyDecorateMessages(): void { + for (const message of this.messages) { + this.assureMessageIsIncludedInSchema(message.target) + this.decorateMessageWithIrisAdditionalProperties(message.target) + } + } - private assureMessageIsIncludedInSchema( - message: T, - ): void { - // In case an empty class is decorated (e.g. "@Message() class Foo {}", then - // no property is found in metadata storage of class-validator. Such classes - // are then also not included in json schema prepared by class-validator-jsonschema - // module. - // One solution is this; additionally decorate "fake" property for each message class - // so it is surely included in the metadata storage and is picked up by json schema - // module. - // Decorate it with with - // - IsOptional() to not have it inside "required" jsonschema property and - // - @Exclude() so that this property is also not described for this schema. - Exclude()(message.prototype, "__keep__"); - IsOptional()(message.prototype, "__keep__"); - IsString()(message.prototype, "__keep__"); - } + private assureMessageIsIncludedInSchema( + message: T, + ): void { + // In case an empty class is decorated (e.g. "@Message() class Foo {}", then + // no property is found in metadata storage of class-validator. Such classes + // are then also not included in json schema prepared by class-validator-jsonschema + // module. + // One solution is this; additionally decorate "fake" property for each message class + // so it is surely included in the metadata storage and is picked up by json schema + // module. + // Decorate it with with + // - IsOptional() to not have it inside "required" jsonschema property and + // - @Exclude() so that this property is also not described for this schema. + Exclude()(message.prototype, '__keep__') + IsOptional()(message.prototype, '__keep__') + IsString()(message.prototype, '__keep__') + } - private isIrisMessage(message: T): boolean { - if (internallyDefinedMessages.includes(message)) { - return true; - } + private isIrisMessage(message: T): boolean { + if (internallyDefinedMessages.includes(message)) { + return true + } - // if this is a message without a handler, it's probably defined by "us" - return ( - this.messageHandlers.find( - (handler) => handler.messageClass === message, - ) === undefined - ); - } + // if this is a message without a handler, it's probably defined by "us" + return ( + this.messageHandlers.find( + (handler) => handler.messageClass === message, + ) === undefined + ) + } - private decorateMessageWithIrisAdditionalProperties( - message: T, - ): void { - const isIrisMessage = this.isIrisMessage(message); - JSONSchema({ - "x-iris-generated": isIrisMessage, - })(message); - } + private decorateMessageWithIrisAdditionalProperties( + message: T, + ): void { + const isIrisMessage = this.isIrisMessage(message) + JSONSchema({ + 'x-iris-generated': isIrisMessage, + })(message) + } } diff --git a/src/lib/asyncapi/schema/iris.ts b/src/lib/asyncapi/schema/iris.ts index 119fad9..a96f6c2 100644 --- a/src/lib/asyncapi/schema/iris.ts +++ b/src/lib/asyncapi/schema/iris.ts @@ -1,144 +1,147 @@ -import _ from "lodash"; -import { ProcessedMessageMetadataI } from "../../message.interfaces"; -import { AsyncapiClassValidator } from "../class_validator"; -import * as interfaces from "../interfaces"; +import _ from 'lodash' +import type { ProcessedMessageMetadataI } from '../../message.interfaces' +import type { AsyncapiClassValidator } from '../class_validator' +import type * as interfaces from '../interfaces' export interface IrisSchemasI { - messages: ProcessedMessageMetadataI[]; - asyncapiClassValidator: AsyncapiClassValidator; + messages: ProcessedMessageMetadataI[] + asyncapiClassValidator: AsyncapiClassValidator } export class IrisSchemas { - private messages: ProcessedMessageMetadataI[]; - private asyncapiClassValidator: AsyncapiClassValidator; - - constructor({ messages, asyncapiClassValidator }: IrisSchemasI) { - this.messages = messages; - this.asyncapiClassValidator = asyncapiClassValidator; - } - - public getSchemasForMessages(): interfaces.SchemaObjects { - const schemas = this.pickRelevantSchemas(); - - return this.fixDateTimeProperties(schemas); - } - - private pickRelevantSchemas(): interfaces.SchemaObjects { - const schemas = this.asyncapiClassValidator.generateJsonSchema(); - const eventSchemas = this.getBaseEventSchemasFromMessages(schemas); - - function searchObjForRefs( - obj?: interfaces.SchemaObject | unknown[] | null, - ): void { - if (obj !== null && (Array.isArray(obj) || typeof obj === "object")) { - for (const prop of Object.values(obj).flat()) { - if (Array.isArray(prop)) { - searchObjForRefs(prop); - } else if (typeof prop === "object") { - if ((<{ $ref?: string }>prop).$ref !== undefined) { - const sname = ( - ((<{ $ref?: string }>prop).$ref).split("/").pop() - ); - if (eventSchemas[sname] === undefined) { - eventSchemas[sname] = sname; - } - searchObjForRefs(schemas[sname]); - } else { - searchObjForRefs(>prop); - } - } - } - } - } - - Object.keys(schemas) - .filter((sname) => eventSchemas[sname] !== undefined) - .map( - (sname) => - (<{ properties?: Record }>( - schemas[eventSchemas[sname]] - )).properties, - ) - .filter((obj) => obj !== undefined) - .forEach(searchObjForRefs); - - return _.reduce( - eventSchemas, - (sc, targetClassName, lookup) => ({ - ...sc, - [lookup]: schemas[targetClassName], - }), - {}, - ); - } - - private getBaseEventSchemasFromMessages( - schemas: interfaces.SchemaObjects, - ): Record { - return this.messages.reduce((acc, msgMeta) => { - const { targetClassName: cName } = msgMeta; - const hasSchema = - schemas[cName] !== undefined; - - if (!hasSchema) { - throw new Error( - `ERR_IRIS_ASYNCAPI_MESSAGE_SCHEMA_NOT_FOUND_FOR: ${cName}`, - ); - } - - return { ...acc, [cName]: cName }; - }, {}); - } - - private fixDateTimeProperties( - schemas: interfaces.SchemaObjects, - ): interfaces.SchemaObjects { - // Date becomes - // oneOf: [{ format: 'date-time', type: string }, { format: 'date', format: string }] - // Java implementation prefers first one and does not handle second one. - - const fixedSchemas = _.merge({}, schemas); - - _(fixedSchemas) - .map((schema) => schema.properties) - .each( - ( - props?: Record< - string, - interfaces.SchemaObject | interfaces.ReferenceObject - >, - ) => { - if (props === undefined) { - return; - } - - _(props) - .values() - .each( - (prop: interfaces.SchemaObject | interfaces.ReferenceObject) => { - if ((prop).oneOf === undefined) { - return; - } - - const soProp = prop; - const oneOf = soProp.oneOf; - - const datesOnly = - oneOf - .map((oo) => oo.format) - .filter((fmt) => fmt === "date" || fmt === "date-time") - .length === 2; - - if (datesOnly) { - soProp.format = "date-time"; - soProp.type = "string"; - delete soProp.oneOf; - } - }, - ); - }, - ); - - return fixedSchemas; - } + private messages: ProcessedMessageMetadataI[] + private asyncapiClassValidator: AsyncapiClassValidator + + constructor({ messages, asyncapiClassValidator }: IrisSchemasI) { + this.messages = messages + this.asyncapiClassValidator = asyncapiClassValidator + } + + public getSchemasForMessages(): interfaces.SchemaObjects { + const schemas = this.pickRelevantSchemas() + + return this.fixDateTimeProperties(schemas) + } + + private pickRelevantSchemas(): interfaces.SchemaObjects { + const schemas = this.asyncapiClassValidator.generateJsonSchema() + const eventSchemas = this.getBaseEventSchemasFromMessages(schemas) + + function searchObjForRefs( + obj?: interfaces.SchemaObject | unknown[] | null, + ): void { + if (obj !== null && (Array.isArray(obj) || typeof obj === 'object')) { + for (const prop of Object.values(obj).flat()) { + if (Array.isArray(prop)) { + searchObjForRefs(prop) + } else if (typeof prop === 'object') { + if ((<{ $ref?: string }>prop).$ref !== undefined) { + const sname = ( + ((<{ $ref?: string }>prop).$ref).split('/').pop() + ) + if (eventSchemas[sname] === undefined) { + eventSchemas[sname] = sname + } + searchObjForRefs(schemas[sname]) + } else { + searchObjForRefs(>prop) + } + } + } + } + } + + Object.keys(schemas) + .filter((sname) => eventSchemas[sname] !== undefined) + .map( + (sname) => + (<{ properties?: Record }>( + schemas[eventSchemas[sname]] + )).properties, + ) + .filter((obj) => obj !== undefined) + .forEach(searchObjForRefs) + + return _.reduce( + eventSchemas, + (sc, targetClassName, lookup) => ({ + ...sc, + [lookup]: schemas[targetClassName], + }), + {}, + ) + } + + private getBaseEventSchemasFromMessages( + schemas: interfaces.SchemaObjects, + ): Record { + return this.messages.reduce((acc, msgMeta) => { + const { targetClassName: cName } = msgMeta + const hasSchema = + schemas[cName] !== undefined + + if (!hasSchema) { + throw new Error( + `ERR_IRIS_ASYNCAPI_MESSAGE_SCHEMA_NOT_FOUND_FOR: ${cName}`, + ) + } + + acc[cName] = cName + + return acc + }, {}) + } + + private fixDateTimeProperties( + schemas: interfaces.SchemaObjects, + ): interfaces.SchemaObjects { + // Date becomes + // oneOf: [{ format: 'date-time', type: string }, { format: 'date', format: string }] + // Java implementation prefers first one and does not handle second one. + + const fixedSchemas = _.merge({}, schemas) + + _(fixedSchemas) + .map((schema) => schema.properties) + .each( + ( + props?: Record< + string, + interfaces.SchemaObject | interfaces.ReferenceObject + >, + ) => { + if (props === undefined) { + return + } + + _(props) + .values() + .each( + (prop: interfaces.SchemaObject | interfaces.ReferenceObject) => { + if ((prop).oneOf === undefined) { + return + } + + const soProp = prop + const oneOf = soProp.oneOf + + const datesOnly = + oneOf + .map((oo) => oo.format) + .filter((fmt) => fmt === 'date' || fmt === 'date-time') + .length === 2 + + if (datesOnly) { + soProp.format = 'date-time' + soProp.type = 'string' + // biome-ignore lint/performance/noDelete: + delete soProp.oneOf + } + }, + ) + }, + ) + + return fixedSchemas + } } diff --git a/src/lib/connection.interfaces.ts b/src/lib/connection.interfaces.ts index ce897db..42826f5 100644 --- a/src/lib/connection.interfaces.ts +++ b/src/lib/connection.interfaces.ts @@ -1,40 +1,39 @@ -import * as amqplib from "amqplib"; +import type * as amqplib from 'amqplib' export interface OptionalConfigI { - /** - * Passed to amqplib, see amqplib doc for more info - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - socketOptions?: any; + /** + * Passed to amqplib, see amqplib doc for more info + */ + socketOptions?: any - /** - * How many times should iris try to reconnect when connection drops - * setting to 0 or less means do not try to reconnect - */ - reconnectTries: number; - /** - * Reconnect interval - */ - reconnectInterval: number; - /** - * Multiply factor for reconnectInterval for each next time if reconnecting fails - */ - reconnectFactor: number; + /** + * How many times should iris try to reconnect when connection drops + * setting to 0 or less means do not try to reconnect + */ + reconnectTries: number + /** + * Reconnect interval + */ + reconnectInterval: number + /** + * Multiply factor for reconnectInterval for each next time if reconnecting fails + */ + reconnectFactor: number - /** - * When error occures during event processing, event is re-enqueued. - * This setting specifies how many times should a single event be re-enqueued - * before marked as failed. - */ - maxMessageRetryCount: number; + /** + * When error occures during event processing, event is re-enqueued. + * This setting specifies how many times should a single event be re-enqueued + * before marked as failed. + */ + maxMessageRetryCount: number } interface ObligatoryConfigI { - urlOrOpts: - | string - | (Omit & { port?: number | string }); + urlOrOpts: + | string + | (Omit & { port?: number | string }) } -export type ConnectionConfigI = ObligatoryConfigI & Partial; +export type ConnectionConfigI = ObligatoryConfigI & Partial -export type ConfigI = ObligatoryConfigI & OptionalConfigI; +export type ConfigI = ObligatoryConfigI & OptionalConfigI diff --git a/src/lib/connection.ts b/src/lib/connection.ts index fd1c5d3..48612b4 100644 --- a/src/lib/connection.ts +++ b/src/lib/connection.ts @@ -1,372 +1,369 @@ -import * as amqplib from "amqplib"; -import _ from "lodash"; -import { LoggerI, getLogger } from "../logger"; -import * as interfaces from "./connection.interfaces"; -import * as constants from "./constants"; -import * as helper from "./helper"; -import * as messageDecoratorUtils from "./message.decorator_utils"; -import { Scope } from "./message.interfaces"; -import * as messageHandlerI from "./message_handler.interfaces"; - -export * from "./connection.interfaces"; - -type ChannelI = amqplib.Channel & { _lookup_key_: string }; -type ChannelsI = { [key: string]: Promise | undefined }; +import * as amqplib from 'amqplib' +import _ from 'lodash' +import logger from '../logger' +import type * as interfaces from './connection.interfaces' +import * as constants from './constants' +import { asError } from './errors' +import * as helper from './helper' +import * as messageDecoratorUtils from './message.decorator_utils' +import { Scope } from './message.interfaces' +import type * as messageHandlerI from './message_handler.interfaces' + +export * from './connection.interfaces' + +type ChannelI = amqplib.Channel & { _lookup_key_: string } +type ChannelsI = { [key: string]: Promise | undefined } type ReconnectGeneratorParamsI = Pick< - interfaces.OptionalConfigI, - "reconnectTries" | "reconnectFactor" | "reconnectInterval" ->; + interfaces.OptionalConfigI, + 'reconnectTries' | 'reconnectFactor' | 'reconnectInterval' +> export class Connection { - private logger: LoggerI; - - private connection: amqplib.Connection | undefined; - private intentionallyDisconnected = false; - private disconnectPromise: Promise | undefined; - private connectPromise: Promise | undefined; - private reconnectHelper: ReconnectHelper | undefined; - private doAutoReconnect = true; - - private channels: ChannelsI = {}; - - private config: interfaces.ConfigI | undefined; - - constructor() { - this.logger = getLogger("Iris:Connection"); - } - - public getConnection(): amqplib.Connection | undefined { - return this.connection; - } - - public getConfig(): interfaces.ConfigI { - return _.cloneDeep(this.config); - } - - public async connect(config: interfaces.ConnectionConfigI): Promise { - this.setDoAutoReconnect(true); - if (this.reconnectHelper !== undefined) { - await this.reconnectHelper.promise; - } - - await this.internalConnect(config); - } - - /** - * Returns true if lib is disconnected and not trying to connect - * or false when lib is either connected or in process of (re)connecting - * - * Can be used for health checks - */ - public isDisconnected(): boolean { - return ( - this.connection === undefined && - this.connectPromise === undefined && - this.reconnectHelper === undefined && - this.disconnectPromise === undefined - ); - } - - public isReconnecting(): boolean { - return this.connection === undefined && this.reconnectHelper !== undefined; - } - - public setDoAutoReconnect(autoReconnect: boolean): void { - this.doAutoReconnect = autoReconnect; - } - - private async internalConnect( - config?: interfaces.ConnectionConfigI, - ): Promise { - if (this.disconnectPromise !== undefined) { - await this.disconnectPromise; - } - - if (this.connection !== undefined) { - this.logger.verbose("Already connected"); - - return; - } - - if (this.connectPromise === undefined) { - if (config !== undefined) { - this.setOptions(config); - } - this.connectPromise = this.doConnect(); - this.connectPromise.catch(() => { - this.connectPromise = undefined; - }); - } else { - this.logger.verbose("Already connecting"); - } - - return this.connectPromise; - } - - private async doConnect(): Promise { - this.logger.log("Connecting"); - const options = this.config; - - this.connection = await amqplib.connect( - options.urlOrOpts, - { - ...options.socketOptions, - // https://github.com/amqp-node/amqplib/issues/217#issuecomment-373728741 - clientProperties: { - ...options.socketOptions?.clientProperties, - connection_name: helper.getHostName(), - }, - }, - ); - this.logger.verbose("Connected"); - this.intentionallyDisconnected = false; - this.connectPromise = undefined; - - this.connection.once("close", (error?: Error) => { - this.onDisconnectCleanup(); - - if (error !== undefined) { - this.logger.errorDetails("Connection errored", { error }); - } else { - this.logger.warn("Connection closed"); - } - - this.reconnect(); - }); - } - - public async disconnect(): Promise { - if (this.reconnectHelper !== undefined) { - await this.reconnectHelper.promise; - } - - if (this.connectPromise !== undefined) { - await this.connectPromise; - } - - if (this.connection === undefined) { - this.logger.verbose("Already disconnected"); - - return; - } - - if (this.disconnectPromise === undefined) { - this.intentionallyDisconnected = true; - this.disconnectPromise = this.doDisconnect(); - } else { - this.logger.verbose("Already disconnecting"); - } - - return this.disconnectPromise; - } - - private async doDisconnect(): Promise { - // if disconnect is called right after publish then - // messages do not come through unless we wait - await new Promise((resolve) => setTimeout(resolve)); - this.logger.log("Disconnecting"); - await (this.connection).close(); - this.onDisconnectCleanup(); - } - - private onDisconnectCleanup(): void { - this.connection = undefined; - this.disconnectPromise = undefined; - this.reconnectHelper = undefined; - } - - public shouldAutoReconnect(): boolean { - return this.doAutoReconnect && !this.intentionallyDisconnected; - } - - public async assureDefaultChannel(): Promise { - return this.assureChannel("defaultChannel"); - } - - public async assureChannelForHandler( - handler: messageHandlerI.ProcessedMessageHandlerMetadataI, - ): Promise { - const { uuid } = handler; - const { prefetch } = handler.processedConfig; - const msgMeta = messageDecoratorUtils.getMessageDecoratedClass( - handler.messageClass, - ); - const isFrontend = msgMeta.origDecoratorConfig.scope === Scope.FRONTEND; - - const key = isFrontend - ? "_frontend_channel_" - : [ - uuid, - `(${handler.targetClassName}.${handler.methodName})`, - msgMeta.uuid, - ].join("-"); - - return this.assureChannel(key, prefetch); - } - - public async assureChannel( - lookup: string, - prefetch?: number, - ): Promise { - if (this.reconnectHelper !== undefined) { - await this.reconnectHelper.promise; - } - - if (this.connectPromise !== undefined) { - await this.connectPromise; - } - - let chP = this.channels[lookup]; - if (chP === undefined) { - chP = this.doAssureChannel(lookup, prefetch); - this.channels[lookup] = chP; - chP.catch(() => { - delete this.channels[lookup]; - }); - } - - return chP; - } - - private async doAssureChannel( - lookup: string, - prefetch?: number, - ): Promise { - this.logger.log(`Opening channel for ${lookup}`); - - if (this.connection === undefined) { - delete this.channels[lookup]; - throw new Error("ERR_IRIS_CONNECTION_NOT_ESTABLISHED"); - } - - const channel = await this.connection.createChannel(); - channel._lookup_key_ = lookup; - - channel.once("close", () => { - this.logger.verbose(`Channel for ${lookup} closed`); - delete this.channels[lookup]; - }); - - if (prefetch !== undefined) { - this.logger.verbose(`Setting prefetch for ${lookup} to ${prefetch}`); - await channel.prefetch(prefetch); - } - - return channel; - } - - private reconnect(): void { - if (!this.shouldAutoReconnect()) { - return; - } - - const options = this.config; - - if (options.reconnectTries < 1) { - this.logger.warn("Reconnecting disabled", { - reconnectTries: options.reconnectTries, - }); - - return; - } - - if (this.reconnectHelper === undefined) { - this.reconnectHelper = new ReconnectHelper(options); - } - - const reconnectDelay = this.reconnectHelper.nextDelay(); - - if (reconnectDelay === false) { - this.logger.errorDetails( - "Reconnecting exhausted, will not try to reconnect", - ); - this.onReconnectCleanup(new Error("ERR_IRIS_CONNECTION_NOT_ESTABLISHED")); - - return; - } - - this.logger.warn(`Reconnecting in ${reconnectDelay}ms`); - - setTimeout(() => { - this.doReconnect(); - }, reconnectDelay); - } - - private async doReconnect(): Promise { - try { - await this.internalConnect(); - this.onReconnectCleanup(); - } catch (err) { - this.logger.errorDetails("Reconnect failed"); - this.reconnect(); - } - } - - private onReconnectCleanup(err?: Error): void { - if (this.reconnectHelper !== undefined) { - if (err === undefined) { - this.reconnectHelper.resolve(); - } else { - this.reconnectHelper.reject(err); - } - } - this.reconnectHelper = undefined; - } - - private setOptions(config: interfaces.ConnectionConfigI): void { - this.config = { - ...constants.CONNECTION_DEFAULT_OPTONS, - ...config, - }; - } + private TAG = 'Iris:Connection' + + private connection: amqplib.Connection | undefined + private intentionallyDisconnected = false + private disconnectPromise: Promise | undefined + private connectPromise: Promise | undefined + private reconnectHelper: ReconnectHelper | undefined + private doAutoReconnect = true + + private channels: ChannelsI = {} + + private config: interfaces.ConfigI | undefined + + public getConnection(): amqplib.Connection | undefined { + return this.connection + } + + public getConfig(): interfaces.ConfigI { + return _.cloneDeep(this.config) + } + + public async connect(config: interfaces.ConnectionConfigI): Promise { + this.setDoAutoReconnect(true) + if (this.reconnectHelper !== undefined) { + await this.reconnectHelper.promise + } + + await this.internalConnect(config) + } + + /** + * Returns true if lib is disconnected and not trying to connect + * or false when lib is either connected or in process of (re)connecting + * + * Can be used for health checks + */ + public isDisconnected(): boolean { + return ( + this.connection === undefined && + this.connectPromise === undefined && + this.reconnectHelper === undefined && + this.disconnectPromise === undefined + ) + } + + public isReconnecting(): boolean { + return this.connection === undefined && this.reconnectHelper !== undefined + } + + public setDoAutoReconnect(autoReconnect: boolean): void { + this.doAutoReconnect = autoReconnect + } + + private async internalConnect( + config?: interfaces.ConnectionConfigI, + ): Promise { + if (this.disconnectPromise !== undefined) { + await this.disconnectPromise + } + + if (this.connection !== undefined) { + logger.debug(this.TAG, 'Already connected') + + return + } + + if (this.connectPromise === undefined) { + if (config !== undefined) { + this.setOptions(config) + } + this.connectPromise = this.doConnect() + this.connectPromise.catch(() => { + this.connectPromise = undefined + }) + } else { + logger.debug(this.TAG, 'Already connecting') + } + + return this.connectPromise + } + + private async doConnect(): Promise { + logger.log(this.TAG, 'Connecting') + const options = this.config + + this.connection = await amqplib.connect( + options.urlOrOpts, + { + ...options.socketOptions, + // https://github.com/amqp-node/amqplib/issues/217#issuecomment-373728741 + clientProperties: { + ...options.socketOptions?.clientProperties, + connection_name: helper.getHostName(), + }, + }, + ) + logger.debug(this.TAG, 'Connected') + this.intentionallyDisconnected = false + this.connectPromise = undefined + + this.connection.once('close', (err?: Error) => { + this.onDisconnectCleanup() + + if (err !== undefined) { + logger.error(this.TAG, 'Connection errored', { err }) + } else { + logger.warn(this.TAG, 'Connection closed') + } + + this.reconnect() + }) + } + + public async disconnect(): Promise { + if (this.reconnectHelper !== undefined) { + await this.reconnectHelper.promise + } + + if (this.connectPromise !== undefined) { + await this.connectPromise + } + + if (this.connection === undefined) { + logger.debug(this.TAG, 'Already disconnected') + + return + } + + if (this.disconnectPromise === undefined) { + this.intentionallyDisconnected = true + this.disconnectPromise = this.doDisconnect() + } else { + logger.debug(this.TAG, 'Already disconnecting') + } + + return this.disconnectPromise + } + + private async doDisconnect(): Promise { + // if disconnect is called right after publish then + // messages do not come through unless we wait + await new Promise((resolve) => setTimeout(resolve)) + logger.log(this.TAG, 'Disconnecting') + await (this.connection).close() + this.onDisconnectCleanup() + } + + private onDisconnectCleanup(): void { + this.connection = undefined + this.disconnectPromise = undefined + this.reconnectHelper = undefined + } + + public shouldAutoReconnect(): boolean { + return this.doAutoReconnect && !this.intentionallyDisconnected + } + + public async assureDefaultChannel(): Promise { + return this.assureChannel('defaultChannel') + } + + public async assureChannelForHandler( + handler: messageHandlerI.ProcessedMessageHandlerMetadataI, + ): Promise { + const { uuid } = handler + const { prefetch } = handler.processedConfig + const msgMeta = messageDecoratorUtils.getMessageDecoratedClass( + handler.messageClass, + ) + const isFrontend = msgMeta.origDecoratorConfig.scope === Scope.FRONTEND + + const key = isFrontend + ? '_frontend_channel_' + : [ + uuid, + `(${handler.targetClassName}.${handler.methodName})`, + msgMeta.uuid, + ].join('-') + + return this.assureChannel(key, prefetch) + } + + public async assureChannel( + lookup: string, + prefetch?: number, + ): Promise { + if (this.reconnectHelper !== undefined) { + await this.reconnectHelper.promise + } + + if (this.connectPromise !== undefined) { + await this.connectPromise + } + + let chP = this.channels[lookup] + if (chP === undefined) { + chP = this.doAssureChannel(lookup, prefetch) + this.channels[lookup] = chP + chP.catch(() => { + delete this.channels[lookup] + }) + } + + return chP + } + + private async doAssureChannel( + lookup: string, + prefetch?: number, + ): Promise { + logger.debug(this.TAG, `Opening channel for ${lookup}`) + + if (this.connection === undefined) { + delete this.channels[lookup] + throw new Error('ERR_IRIS_CONNECTION_NOT_ESTABLISHED') + } + + const channel = await this.connection.createChannel() + channel._lookup_key_ = lookup + + channel.once('close', () => { + logger.debug(this.TAG, `Channel for ${lookup} closed`) + delete this.channels[lookup] + }) + + if (prefetch !== undefined) { + logger.debug(this.TAG, `Setting prefetch for ${lookup} to ${prefetch}`) + await channel.prefetch(prefetch) + } + + return channel + } + + private reconnect(): void { + if (!this.shouldAutoReconnect()) { + return + } + + const options = this.config + + if (options.reconnectTries < 1) { + logger.warn(this.TAG, 'Reconnecting disabled', { + reconnectTries: options.reconnectTries, + }) + + return + } + + if (this.reconnectHelper === undefined) { + this.reconnectHelper = new ReconnectHelper(options) + } + + const reconnectDelay = this.reconnectHelper.nextDelay() + + if (reconnectDelay === false) { + logger.error( + this.TAG, + 'Reconnecting exhausted, will not try to reconnect', + ) + this.onReconnectCleanup(new Error('ERR_IRIS_CONNECTION_NOT_ESTABLISHED')) + + return + } + + logger.warn(this.TAG, `Reconnecting in ${reconnectDelay}ms`) + + setTimeout(() => { + this.doReconnect() + }, reconnectDelay) + } + + private async doReconnect(): Promise { + try { + await this.internalConnect() + this.onReconnectCleanup() + } catch (err) { + logger.error(this.TAG, 'Reconnect failed', { err: asError(err) }) + this.reconnect() + } + } + + private onReconnectCleanup(err?: Error): void { + if (this.reconnectHelper !== undefined) { + if (err === undefined) { + this.reconnectHelper.resolve() + } else { + this.reconnectHelper.reject(err) + } + } + this.reconnectHelper = undefined + } + + private setOptions(config: interfaces.ConnectionConfigI): void { + this.config = { + ...constants.CONNECTION_DEFAULT_OPTONS, + ...config, + } + } } export class ReconnectHelper { - public promise: Promise; - public resolve!: () => void; - public reject!: (err: Error) => void; - private delayGenerator: Generator; - private logger: LoggerI; - - constructor(conf: ReconnectGeneratorParamsI) { - this.logger = getLogger("Iris:Connection:ReconnectHelper"); - this.delayGenerator = this.getReconnectDelayGenerator(conf); - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }); - } - - public nextDelay(): number | false { - return this.delayGenerator.next().value; - } - - private getReconnectDelayGenerator({ - reconnectInterval, - reconnectFactor, - reconnectTries, - }: ReconnectGeneratorParamsI): Generator { - const logger = this.logger; - function* nextDelay(): Generator { - let reconnectTryNum = 0; - while (reconnectTryNum < reconnectTries) { - logger.verbose("Generating next delay", { - reconnectInterval, - reconnectFactor, - reconnectTryNum, - }); - yield Math.round( - reconnectInterval + - reconnectTryNum * reconnectInterval * reconnectFactor, - ); - reconnectTryNum = reconnectTryNum + 1; - } - - return false; - } - - return nextDelay(); - } + public promise: Promise + public resolve!: () => void + public reject!: (err: Error) => void + private delayGenerator: Generator + + private static TAG = 'Iris:Connection:ReconnectHelper' + + constructor(conf: ReconnectGeneratorParamsI) { + this.delayGenerator = this.getReconnectDelayGenerator(conf) + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve + this.reject = reject + }) + } + + public nextDelay(): number | false { + return this.delayGenerator.next().value + } + + private getReconnectDelayGenerator({ + reconnectInterval, + reconnectFactor, + reconnectTries, + }: ReconnectGeneratorParamsI): Generator { + function* nextDelay(): Generator { + let reconnectTryNum = 0 + while (reconnectTryNum < reconnectTries) { + logger.debug(ReconnectHelper.TAG, 'Generating next delay', { + reconnectInterval, + reconnectFactor, + reconnectTryNum, + }) + yield Math.round( + reconnectInterval + + reconnectTryNum * reconnectInterval * reconnectFactor, + ) + reconnectTryNum = reconnectTryNum + 1 + } + + return false + } + + return nextDelay() + } } -export const connection = new Connection(); +export const connection = new Connection() diff --git a/src/lib/constants.ts b/src/lib/constants.ts index cf65fb1..4d03e9a 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,297 +1,307 @@ -import * as amqplib from "amqplib"; -import * as connectionI from "./connection.interfaces"; -import * as messageI from "./message.interfaces"; +import type * as amqplib from 'amqplib' +import type * as connectionI from './connection.interfaces' +import * as messageI from './message.interfaces' -let REINITIALIZATION_DELAY = 5000; +let REINITIALIZATION_DELAY = 5000 -const DEFAULT_TTL = 15000; +const DEFAULT_TTL = 15000 type ExchangeDefaultsI = { - exchangeOptions: messageI.AssertExchangeI; - exchangeType: messageI.ExchangeType; - scope: messageI.Scope; -}; + exchangeOptions: messageI.AssertExchangeI + exchangeType: messageI.ExchangeType + scope: messageI.Scope +} type QueueDefaultsI = { - durable: boolean; - autoDelete: boolean; -}; + durable: boolean + autoDelete: boolean +} export type ManagedExchangeI = { - EXCHANGE: string; - EXCHANGE_TYPE: messageI.ExchangeType; - EXCHANGE_OPTIONS?: messageI.AssertExchangeI; -}; + EXCHANGE: string + EXCHANGE_TYPE: messageI.ExchangeType + EXCHANGE_OPTIONS?: messageI.AssertExchangeI +} function castToQueueOptions(opts: T): T { - return opts; + return opts } function castToExchangeOptions(opts: T): T { - return opts; + return opts } -export const getReinitializationDelay = (): number => REINITIALIZATION_DELAY; +export const getReinitializationDelay = (): number => REINITIALIZATION_DELAY export const setReinitializationDelay = (delay: number): void => { - REINITIALIZATION_DELAY = delay; -}; + REINITIALIZATION_DELAY = delay +} export enum ManagedExchangesE { - FRONTEND = "frontend", - DEAD_LETTER = "dead.dead-letter", - ERROR = "error", - RETRY = "retry", - BROADCAST = "broadcast", - SESSION = "session", - USER = "user", - SUBSCRIPTION = "subscription", - SNAPSHOT_REQUESTED = "snapshot-requested", - SUBSCRIBE_INTERNAL = "subscribe-internal", + FRONTEND = 'frontend', + DEAD_LETTER = 'dead.dead-letter', + ERROR = 'error', + RETRY = 'retry', + BROADCAST = 'broadcast', + SESSION = 'session', + USER = 'user', + SUBSCRIPTION = 'subscription', + SNAPSHOT_REQUESTED = 'snapshot-requested', + SUBSCRIBE_INTERNAL = 'subscribe-internal', } type ManagedExchangesReversedI = Record< - ManagedExchangesE, - keyof typeof ManagedExchangesE ->; + ManagedExchangesE, + keyof typeof ManagedExchangesE +> const managedExchangesReversed: ManagedExchangesReversedI = Object.keys( - ManagedExchangesE, + ManagedExchangesE, ).reduce( - (rev, managedExchangeKey): ManagedExchangesReversedI => { - return { - ...rev, - [ManagedExchangesE[managedExchangeKey]]: managedExchangeKey, - }; - }, - ({}), -); + (rev, managedExchangeKey): ManagedExchangesReversedI => { + rev[ManagedExchangesE[managedExchangeKey]] = managedExchangeKey + return rev + }, + ({}), +) const NON_RESERVED_EXCHANGE_NAMES = [ - ManagedExchangesE.SUBSCRIPTION, - ManagedExchangesE.SNAPSHOT_REQUESTED, - ManagedExchangesE.SUBSCRIBE_INTERNAL, -]; + ManagedExchangesE.SUBSCRIPTION, + ManagedExchangesE.SNAPSHOT_REQUESTED, + ManagedExchangesE.SUBSCRIBE_INTERNAL, +] export const CONNECTION_DEFAULT_OPTONS: Omit< - connectionI.OptionalConfigI, - "socketOptions" + connectionI.OptionalConfigI, + 'socketOptions' > = { - reconnectTries: 5, - reconnectInterval: 3000, - reconnectFactor: 1.5, - maxMessageRetryCount: 3, -}; + reconnectTries: 5, + reconnectInterval: 3000, + reconnectFactor: 1.5, + maxMessageRetryCount: 3, +} export const MESSAGE_HEADERS = { - MESSAGE: { - ANONYMOUS_ID: "x-anon-id", - CLIENT_TRACE_ID: "x-client-trace-id", - CURRENT_SERVICE_ID: "x-current-service-id", - DEVICE: "x-device", - EVENT_TYPE: "x-event-type", - INSTANCE_ID: "x-instance-id", - IP_ADDRESS: "x-ip-address", - JWT: "x-jwt", - ORIGIN_SERVICE_ID: "x-origin-service-id", - PROXY_IP_ADDRESS: "x-proxy-ip-address", - REQUEST_REFERER: "x-request-referer", - REQUEST_URI: "x-request-uri", - REQUEST_VIA: "x-request-via", - ROUTER: "x-router", - SERVER_TIMESTAMP: "x-server-timestamp", - SESSION_ID: "x-session-id", - SUBSCRIPTION_ID: "x-subscription-id", - USER_AGENT: "x-user-agent", - USER_ID: "x-user-id", - }, - - REQUEUE: { - ERROR_CODE: "x-error-code", - ERROR_TYPE: "x-error-type", - ERROR_MESSAGE: "x-error-message", - MAX_RETRIES: "x-max-retries", - NOTIFY_CLIENT: "x-notify-client", - ORIGINAL_EXCHANGE: "x-original-exchange", - ORIGINAL_ROUTING_KEY: "x-original-routing-key", - ORIGINAL_QUEUE: "x-original-queue", - RETRY_COUNT: "x-retry-count", - }, - - QUEUE_DECLARATION: { - DEAD_LETTER_EXCHANGE: "x-dead-letter-exchange", - DEAD_LETTER_ROUTING_KEY: "x-dead-letter-routing-key", - MESSAGE_TTL: "x-message-ttl", - }, -}; + MESSAGE: { + ANONYMOUS_ID: 'x-anon-id', + CACHE_TTL: 'x-cache-ttl', + CLIENT_TRACE_ID: 'x-client-trace-id', + CLIENT_VERSION: 'x-client-version', + CORRELATION_ID: 'x-correlation-id', + CURRENT_SERVICE_ID: 'x-current-service-id', + DEVICE: 'x-device', + EVENT_TYPE: 'x-event-type', + INSTANCE_ID: 'x-instance-id', + IP_ADDRESS: 'x-ip-address', + JWT: 'x-jwt', + ORIGIN_SERVICE_ID: 'x-origin-service-id', + PROXY_IP_ADDRESS: 'x-proxy-ip-address', + REQUEST_REFERER: 'x-request-referer', + REQUEST_URI: 'x-request-uri', + REQUEST_VIA: 'x-request-via', + ROUTER: 'x-router', + SERVER_TIMESTAMP: 'x-server-timestamp', + SESSION_ID: 'x-session-id', + SUBSCRIPTION_ID: 'x-subscription-id', + USER_AGENT: 'x-user-agent', + USER_ID: 'x-user-id', + }, + + REQUEUE: { + ERROR_CODE: 'x-error-code', + ERROR_TYPE: 'x-error-type', + ERROR_MESSAGE: 'x-error-message', + MAX_RETRIES: 'x-max-retries', + NOTIFY_CLIENT: 'x-notify-client', + ORIGINAL_EXCHANGE: 'x-original-exchange', + ORIGINAL_ROUTING_KEY: 'x-original-routing-key', + ORIGINAL_QUEUE: 'x-original-queue', + RETRY_COUNT: 'x-retry-count', + }, + + QUEUE_DECLARATION: { + DEAD_LETTER_EXCHANGE: 'x-dead-letter-exchange', + DEAD_LETTER_ROUTING_KEY: 'x-dead-letter-routing-key', + MESSAGE_TTL: 'x-message-ttl', + }, +} as const const DEFAULTS: { EXCHANGE: ExchangeDefaultsI; QUEUE: QueueDefaultsI } = { - EXCHANGE: { - exchangeOptions: castToExchangeOptions({ - durable: true, - autoDelete: false, - }), - exchangeType: messageI.ExchangeType.fanout, - scope: messageI.Scope.INTERNAL, - }, - QUEUE: { - durable: true, - autoDelete: false, - }, -}; + EXCHANGE: { + exchangeOptions: castToExchangeOptions({ + durable: true, + autoDelete: false, + }), + exchangeType: messageI.ExchangeType.fanout, + scope: messageI.Scope.INTERNAL, + }, + QUEUE: { + durable: true, + autoDelete: false, + }, +} export const MANAGED_EXCHANGES = { - FRONTEND: { - EXCHANGE: ManagedExchangesE.FRONTEND, - EXCHANGE_OPTIONS: castToExchangeOptions({ - durable: true, - autoDelete: false, - }), - QUEUE_OPTIONS: castToQueueOptions({ - durable: true, - autoDelete: false, - messageTtl: DEFAULT_TTL, - }), - EXCHANGE_TYPE: messageI.ExchangeType.topic, - SUFFIX: "frontend", - TTL: DEFAULT_TTL, - }, - - DEAD_LETTER: { - EXCHANGE: ManagedExchangesE.DEAD_LETTER, - EXCHANGE_TYPE: messageI.ExchangeType.topic, - QUEUE: ManagedExchangesE.DEAD_LETTER, - PREFIX: "dead.", - EXCHANGE_OPTIONS: castToExchangeOptions({ - durable: true, - autoDelete: false, - }), - QUEUE_OPTIONS: castToQueueOptions({ durable: true, autoDelete: false }), - }, - - ERROR: { - EXCHANGE: ManagedExchangesE.ERROR, - ROUTING_KEY_SUFFIX: ".error", - EXCHANGE_TYPE: messageI.ExchangeType.topic, - EXCHANGE_OPTIONS: castToExchangeOptions({ - durable: true, - autoDelete: false, - }), - QUEUE: "error", - }, - - RETRY: { - EXCHANGE: ManagedExchangesE.RETRY, - ROUTING_KEY: "retry", - EXCHANGE_TYPE: messageI.ExchangeType.direct, - EXCHANGE_OPTIONS: castToExchangeOptions({ - durable: true, - autoDelete: false, - }), - QUEUE: ManagedExchangesE.RETRY, - WAIT_TTL_PREFIX: "retry.wait-", - WAIT_ENDED: "retry.wait-ended", - }, - - BROADCAST: { - EXCHANGE: ManagedExchangesE.BROADCAST, - EXCHANGE_TYPE: messageI.ExchangeType.topic, - EXCHANGE_OPTIONS: castToExchangeOptions({ - durable: true, - autoDelete: false, - }), - }, - - SESSION: { - EXCHANGE: ManagedExchangesE.SESSION, - EXCHANGE_TYPE: messageI.ExchangeType.topic, - EXCHANGE_OPTIONS: castToExchangeOptions({ - durable: true, - autoDelete: false, - }), - }, - - USER: { - EXCHANGE: ManagedExchangesE.USER, - EXCHANGE_TYPE: messageI.ExchangeType.topic, - EXCHANGE_OPTIONS: castToExchangeOptions({ - durable: true, - autoDelete: false, - }), - }, - - SUBSCRIPTION: { - EXCHANGE: ManagedExchangesE.SUBSCRIPTION, - EXCHANGE_TYPE: messageI.ExchangeType.topic, - EXCHANGE_OPTIONS: castToExchangeOptions({ - durable: true, - autoDelete: false, - }), - }, - - SNAPSHOT_REQUESTED: { - EXCHANGE: ManagedExchangesE.SNAPSHOT_REQUESTED, - EXCHANGE_TYPE: messageI.ExchangeType.topic, - EXCHANGE_OPTIONS: castToExchangeOptions({ - durable: true, - autoDelete: false, - }), - QUEUE_OPTIONS: castToQueueOptions({ durable: true, autoDelete: true }), - }, - - SUBSCRIBE_INTERNAL: { - EXCHANGE: ManagedExchangesE.SUBSCRIBE_INTERNAL, - EXCHANGE_TYPE: messageI.ExchangeType.fanout, - EXCHANGE_OPTIONS: castToExchangeOptions({ - durable: true, - autoDelete: false, - }), - }, -}; + FRONTEND: { + EXCHANGE: ManagedExchangesE.FRONTEND, + EXCHANGE_OPTIONS: castToExchangeOptions({ + durable: true, + autoDelete: false, + }), + QUEUE_OPTIONS: castToQueueOptions({ + durable: true, + autoDelete: false, + messageTtl: DEFAULT_TTL, + }), + EXCHANGE_TYPE: messageI.ExchangeType.topic, + SUFFIX: 'frontend', + TTL: DEFAULT_TTL, + }, + + DEAD_LETTER: { + EXCHANGE: ManagedExchangesE.DEAD_LETTER, + EXCHANGE_TYPE: messageI.ExchangeType.topic, + QUEUE: ManagedExchangesE.DEAD_LETTER, + PREFIX: 'dead.', + EXCHANGE_OPTIONS: castToExchangeOptions({ + durable: true, + autoDelete: false, + }), + QUEUE_OPTIONS: castToQueueOptions({ durable: true, autoDelete: false }), + }, + + ERROR: { + EXCHANGE: ManagedExchangesE.ERROR, + ROUTING_KEY_SUFFIX: '.error', + EXCHANGE_TYPE: messageI.ExchangeType.topic, + EXCHANGE_OPTIONS: castToExchangeOptions({ + durable: true, + autoDelete: false, + }), + QUEUE: 'error', + }, + + RETRY: { + EXCHANGE: ManagedExchangesE.RETRY, + ROUTING_KEY: 'retry', + EXCHANGE_TYPE: messageI.ExchangeType.direct, + EXCHANGE_OPTIONS: castToExchangeOptions({ + durable: true, + autoDelete: false, + }), + QUEUE: ManagedExchangesE.RETRY, + WAIT_TTL_PREFIX: 'retry.wait-', + WAIT_ENDED: 'retry.wait-ended', + }, + + BROADCAST: { + EXCHANGE: ManagedExchangesE.BROADCAST, + EXCHANGE_TYPE: messageI.ExchangeType.topic, + EXCHANGE_OPTIONS: castToExchangeOptions({ + durable: true, + autoDelete: false, + }), + }, + + SESSION: { + EXCHANGE: ManagedExchangesE.SESSION, + EXCHANGE_TYPE: messageI.ExchangeType.topic, + EXCHANGE_OPTIONS: castToExchangeOptions({ + durable: true, + autoDelete: false, + }), + }, + + USER: { + EXCHANGE: ManagedExchangesE.USER, + EXCHANGE_TYPE: messageI.ExchangeType.topic, + EXCHANGE_OPTIONS: castToExchangeOptions({ + durable: true, + autoDelete: false, + }), + }, + + SUBSCRIPTION: { + EXCHANGE: ManagedExchangesE.SUBSCRIPTION, + EXCHANGE_TYPE: messageI.ExchangeType.topic, + EXCHANGE_OPTIONS: castToExchangeOptions({ + durable: true, + autoDelete: false, + }), + }, + + SNAPSHOT_REQUESTED: { + EXCHANGE: ManagedExchangesE.SNAPSHOT_REQUESTED, + EXCHANGE_TYPE: messageI.ExchangeType.topic, + EXCHANGE_OPTIONS: castToExchangeOptions({ + durable: true, + autoDelete: false, + }), + QUEUE_OPTIONS: castToQueueOptions({ durable: true, autoDelete: true }), + }, + + SUBSCRIBE_INTERNAL: { + EXCHANGE: ManagedExchangesE.SUBSCRIBE_INTERNAL, + EXCHANGE_TYPE: messageI.ExchangeType.fanout, + EXCHANGE_OPTIONS: castToExchangeOptions({ + durable: true, + autoDelete: false, + }), + }, +} + +export const MDC_PROPERTIES = { + SESSION_ID: 'sessionId', + USER_ID: 'userId', + CLIENT_TRACE_ID: 'clientTraceId', + CORRELATION_ID: 'correlationId', + EVENT_TYPE: 'eventType', + CLIENT_VERSION: 'clientVersion', +} as const const RESERVED_EXCHANGE_NAMES = Object.values(ManagedExchangesE).filter( - (ex) => !NON_RESERVED_EXCHANGE_NAMES.includes(ex), -); + (ex) => !NON_RESERVED_EXCHANGE_NAMES.includes(ex), +) const RESERVED_QUEUE_NAMES = [ - MANAGED_EXCHANGES.DEAD_LETTER.QUEUE, - MANAGED_EXCHANGES.ERROR.QUEUE, - MANAGED_EXCHANGES.RETRY.QUEUE, - MANAGED_EXCHANGES.RETRY.WAIT_ENDED, -]; + MANAGED_EXCHANGES.DEAD_LETTER.QUEUE, + MANAGED_EXCHANGES.ERROR.QUEUE, + MANAGED_EXCHANGES.RETRY.QUEUE, + MANAGED_EXCHANGES.RETRY.WAIT_ENDED, +] export const getReservedNames = (): string[] => [ - ...RESERVED_EXCHANGE_NAMES, - ...RESERVED_QUEUE_NAMES, -]; + ...RESERVED_EXCHANGE_NAMES, + ...RESERVED_QUEUE_NAMES, +] export function getExchangeDefaultsForExchangeName( - exchangeName: string, + exchangeName: string, ): ExchangeDefaultsI { - const managedExchange = ( - MANAGED_EXCHANGES[managedExchangesReversed[exchangeName]] - ); - - return { - exchangeOptions: - managedExchange?.EXCHANGE_OPTIONS ?? DEFAULTS.EXCHANGE.exchangeOptions, - exchangeType: - managedExchange?.EXCHANGE_TYPE ?? DEFAULTS.EXCHANGE.exchangeType, - scope: DEFAULTS.EXCHANGE.scope, - }; + const managedExchange = ( + MANAGED_EXCHANGES[managedExchangesReversed[exchangeName]] + ) + + return { + exchangeOptions: + managedExchange?.EXCHANGE_OPTIONS ?? DEFAULTS.EXCHANGE.exchangeOptions, + exchangeType: + managedExchange?.EXCHANGE_TYPE ?? DEFAULTS.EXCHANGE.exchangeType, + scope: DEFAULTS.EXCHANGE.scope, + } } export function getQueueDefaultsForExchangeName( - exchangeName: string, - overrideOptions: Partial, + exchangeName: string, + overrideOptions: Partial, ): QueueDefaultsI { - const managedExchange = <{ QUEUE_OPTIONS?: QueueDefaultsI } | undefined>( - MANAGED_EXCHANGES[managedExchangesReversed[exchangeName]] - ); - - return { - ...DEFAULTS.QUEUE, - ...overrideOptions, - // defaults for managed exchange should prevail even with overrides - ...managedExchange?.QUEUE_OPTIONS, - }; + const managedExchange = <{ QUEUE_OPTIONS?: QueueDefaultsI } | undefined>( + MANAGED_EXCHANGES[managedExchangesReversed[exchangeName]] + ) + + return { + ...DEFAULTS.QUEUE, + ...overrideOptions, + // defaults for managed exchange should prevail even with overrides + ...managedExchange?.QUEUE_OPTIONS, + } } diff --git a/src/lib/consume.ack.ts b/src/lib/consume.ack.ts index f3a57ae..2a9297a 100644 --- a/src/lib/consume.ack.ts +++ b/src/lib/consume.ack.ts @@ -1,57 +1,62 @@ -import * as amqplib from "amqplib"; -import { getLogger } from "../logger"; +import type * as amqplib from 'amqplib' +import logger from '../logger' +import { asError } from './errors' -const logger = getLogger("Iris:Consumer.Ack"); -const nonAckedMsgs: amqplib.ConsumeMessage[] = []; +const TAG = 'Iris:Consumer.Ack' + +const nonAckedMsgs: amqplib.ConsumeMessage[] = [] export function safeAckMsg( - msg: amqplib.ConsumeMessage, - channel: amqplib.Channel, - method: "ack" | "nack" | "reject", - enqueue?: boolean, + msg: amqplib.ConsumeMessage, + channel: amqplib.Channel, + method: 'ack' | 'nack' | 'reject', + enqueue?: boolean, ): void { - try { - switch (method) { - case "reject": - channel.reject(msg, enqueue); - break; - case "nack": - channel.nack(msg, undefined, enqueue); - break; - //case "ack": - default: - channel.ack(msg); - break; - } - } catch (e) { - logger.error("SafeAckMsg can not (n)ack message", e, { - method, - enqueue, - }); - if (method === "ack") { - nonAckedMsgs.push(msg); - } - } + try { + switch (method) { + case 'reject': + channel.reject(msg, enqueue) + break + case 'nack': + channel.nack(msg, undefined, enqueue) + break + //case "ack": + default: + channel.ack(msg) + break + } + } catch (err) { + logger.error(TAG, 'SafeAckMsg can not (n)ack message', { + err: asError(err), + info: { + method, + enqueue, + }, + }) + if (method === 'ack') { + nonAckedMsgs.push(msg) + } + } } export function ignoreMsg( - msg: amqplib.ConsumeMessage, - channel: amqplib.Channel, + msg: amqplib.ConsumeMessage, + channel: amqplib.Channel, ): boolean { - if (!msg.fields.redelivered) { - return false; - } + if (!msg.fields.redelivered) { + return false + } - for (let pos = 0; pos < nonAckedMsgs.length; pos++) { - const nonAckedMsg = nonAckedMsgs[pos]; - if (Buffer.compare(nonAckedMsg.content, msg.content) === 0) { - nonAckedMsgs.splice(pos, 1); - safeAckMsg(msg, channel, "ack"); - logger.log("IgnoreMsg ACKing previously unacked msg"); + for (let pos = 0; pos < nonAckedMsgs.length; pos++) { + const nonAckedMsg = nonAckedMsgs[pos] + if (Buffer.compare(nonAckedMsg.content, msg.content) === 0) { + nonAckedMsgs.splice(pos, 1) + safeAckMsg(msg, channel, 'ack') + logger.warn(TAG, 'IgnoreMsg ACKing previously unacked msg') - return true; - } - } + return true + } + } - return false; + return false } diff --git a/src/lib/consume.error.ts b/src/lib/consume.error.ts index da87ee7..1add8f1 100644 --- a/src/lib/consume.error.ts +++ b/src/lib/consume.error.ts @@ -1,96 +1,108 @@ -import * as amqplib from "amqplib"; -import { getLogger } from "../logger"; +import type * as amqplib from 'amqplib' +import logger from '../logger' import { - cloneAmqpMsgProperties, - getTemporaryChannel, - hasClientContext, -} from "./amqp.helper"; -import { MANAGED_EXCHANGES, MESSAGE_HEADERS } from "./constants"; -import * as consumeAck from "./consume.ack"; -import * as consumeRetry from "./consume.retry"; -import * as errors from "./errors"; -import * as messageI from "./message.interfaces"; -import * as messageHandler from "./message_handler"; + cloneAmqpMsgProperties, + getTemporaryChannel, + hasClientContext, +} from './amqp.helper' +import { MANAGED_EXCHANGES, MESSAGE_HEADERS } from './constants' +import * as consumeAck from './consume.ack' +import * as consumeRetry from './consume.retry' +import * as errors from './errors' +import { amqpToMDC } from './mdc' +import type * as messageI from './message.interfaces' +import type * as messageHandler from './message_handler' -const { ERROR } = MANAGED_EXCHANGES; +const { ERROR } = MANAGED_EXCHANGES -const logger = getLogger("Iris:Consumer:HandleError"); +const TAG = 'Iris:Consumer:HandleError' export async function handleConsumeError( - error: Error, - handler: messageHandler.ProcessedMessageHandlerMetadataI, - msgMeta: messageI.ProcessedMessageMetadataI, - channel: amqplib.Channel, - msg: amqplib.ConsumeMessage, + error: Error, + handler: messageHandler.ProcessedMessageHandlerMetadataI, + msgMeta: messageI.ProcessedMessageMetadataI, + channel: amqplib.Channel, + msg: amqplib.ConsumeMessage, ): Promise { - const reject = errors.isRejectableError(error); - const { exchange } = handler.processedConfig; - logger.errorDetails( - "Event consume error", - errors.enhancedDetails( - { - rejecting: reject, - exchange, - handler: handler.processedConfig, - }, - error, - ), - ); + const reject = errors.isRejectableError(error) + const { exchange } = handler.processedConfig + logger.error(TAG, `Event consume error on "${msg.fields.exchange}"`, { + mdc: amqpToMDC(msg), + ...errors.enhancedDetails( + { + rejecting: reject, + exchange, + handler: handler.processedConfig, + }, + error, + ), + }) - if (reject) { - consumeAck.safeAckMsg(msg, channel, "reject", false); - await handleRejectableError(msg, error); - } else { - const enqueued = await consumeRetry.enqueueWithBackoff( - msg, - handler, - msgMeta.processedConfig, - error, - ); - if (enqueued) { - consumeAck.safeAckMsg(msg, channel, "ack"); - } else { - consumeAck.safeAckMsg(msg, channel, "nack"); - } - } + if (reject) { + consumeAck.safeAckMsg(msg, channel, 'reject', false) + await handleRejectableError(msg, error) + } else { + const enqueued = await consumeRetry.enqueueWithBackoff( + msg, + handler, + msgMeta.processedConfig, + error, + ) + if (enqueued) { + consumeAck.safeAckMsg(msg, channel, 'ack') + } else { + consumeAck.safeAckMsg(msg, channel, 'nack') + } + } } async function handleRejectableError( - msg: amqplib.ConsumeMessage, - error: Error, + msg: amqplib.ConsumeMessage, + error: Error, ): Promise { - const notifyClient = errors.shouldNotifyClient(error, msg); + const notifyClient = errors.shouldNotifyClient(error, msg) - if (notifyClient === false) { - logger.debug("Not publishing error message"); + if (notifyClient === false) { + logger.debug(TAG, 'Not publishing error message') - return; - } + return + } - if (hasClientContext(msg)) { - logger.debug("Publishing error message"); - } else { - logger.errorDetails( - "Publishing error message even though msg does not have client context", - ); - } + if (hasClientContext(msg)) { + logger.debug(TAG, 'Publishing error message') + } else { + logger.error( + TAG, + 'Publishing error message even though msg does not have client context', + { + mdc: amqpToMDC(msg), + }, + ) + } - try { - const msgProperties = cloneAmqpMsgProperties(msg); - const { headers } = msgProperties; - delete headers[MESSAGE_HEADERS.MESSAGE.JWT]; - headers[MESSAGE_HEADERS.MESSAGE.EVENT_TYPE] = ERROR.EXCHANGE; - headers[MESSAGE_HEADERS.MESSAGE.SERVER_TIMESTAMP] = Date.now(); - const routingKey = `${msg.fields.exchange}${ERROR.ROUTING_KEY_SUFFIX}`; + try { + const msgProperties = cloneAmqpMsgProperties(msg) + const { headers } = msgProperties + delete headers[MESSAGE_HEADERS.MESSAGE.JWT] + headers[MESSAGE_HEADERS.MESSAGE.EVENT_TYPE] = ERROR.EXCHANGE + headers[MESSAGE_HEADERS.MESSAGE.SERVER_TIMESTAMP] = Date.now() + const routingKey = `${msg.fields.exchange}${ERROR.ROUTING_KEY_SUFFIX}` - const channel = await getTemporaryChannel("error"); - channel.publish( - ERROR.EXCHANGE, - routingKey, - Buffer.from(JSON.stringify(errors.getErrorMessage(error))), - msgProperties, - ); - } catch (e) { - logger.error("Failed to publish error message", e); - } + const channel = await getTemporaryChannel('error') + channel.publish( + ERROR.EXCHANGE, + routingKey, + Buffer.from(JSON.stringify(errors.getErrorMessage(error))), + msgProperties, + ) + } catch (err) { + logger.error( + TAG, + `Failed to publish error message for "${msg.fields.exchange}"`, + { + err: errors.asError(err), + mdc: amqpToMDC(msg), + }, + ) + } } diff --git a/src/lib/consume.handle.ts b/src/lib/consume.handle.ts index 287b7d6..068d80f 100644 --- a/src/lib/consume.handle.ts +++ b/src/lib/consume.handle.ts @@ -1,103 +1,115 @@ -import * as amqplib from "amqplib"; -import * as classTransformer from "class-transformer"; -import * as _ from "lodash"; -import { getLogger } from "../logger"; -import * as amqpHelper from "./amqp.helper"; -import * as consumeAck from "./consume.ack"; -import * as consumeError from "./consume.error"; -import * as errors from "./errors"; -import * as message from "./message"; -import * as messageHandler from "./message_handler"; -import * as publish from "./publish"; +import type * as amqplib from 'amqplib' +import type * as classTransformer from 'class-transformer' +import _ from 'lodash' +import logger from '../logger' +import * as amqpHelper from './amqp.helper' +import * as consumeAck from './consume.ack' +import * as consumeError from './consume.error' +import * as errors from './errors' +import { amqpToMDC } from './mdc' +import type * as message from './message' +import type * as messageHandler from './message_handler' +import * as publish from './publish' -const logger = getLogger("Iris:ConsumerHandle"); +const TAG = 'Iris:ConsumerHandle' type ResolveMessageHandlerI = ( - msg: amqplib.ConsumeMessage, -) => messageHandler.ProcessedMessageHandlerMetadataI; + msg: amqplib.ConsumeMessage, +) => messageHandler.ProcessedMessageHandlerMetadataI type HandleMessageT = { - resolveMessageHandler: ResolveMessageHandlerI; - obtainChannel: () => Promise; - queueName: string; - onChannelClose: () => void; - msgMeta: message.ProcessedMessageMetadataI; -}; + resolveMessageHandler: ResolveMessageHandlerI + obtainChannel: () => Promise + queueName: string + onChannelClose: () => void + msgMeta: message.ProcessedMessageMetadataI +} type HandleMessageReturnT = ( - msg: amqplib.ConsumeMessage | null, -) => Promise; + msg: amqplib.ConsumeMessage | null, +) => Promise export function getMessageHandler({ - resolveMessageHandler, - obtainChannel, - queueName, - onChannelClose, - msgMeta, + resolveMessageHandler, + obtainChannel, + queueName, + onChannelClose, + msgMeta, }: HandleMessageT): HandleMessageReturnT { - return async (msg: amqplib.ConsumeMessage | null): Promise => { - if (msg === null) { - logger.warn( - `Received empty message on queue "${queueName}" (disappeared?)`, - ); - onChannelClose(); + return async (msg: amqplib.ConsumeMessage | null): Promise => { + if (msg === null) { + logger.warn( + TAG, + `Received empty message on queue "${queueName}" (disappeared?)`, + ) + onChannelClose() + + return + } - return; - } + const ch = await obtainChannel() + logger.debug( + TAG, + `Message received for exchange "${msg.fields.exchange}"`, + { + mdc: amqpToMDC(msg), + queueName, + message: msg.content.toString(), + fields: msg.fields, + headers: + amqpHelper.safeAmqpObjectForLogging(msg.properties.headers) ?? + '', + }, + ) - const ch = await obtainChannel(); - logger.debug("Message received for exchange", { - queueName, - message: msg.content.toString(), - fields: msg.fields, - headers: - ( - amqpHelper.safeAmqpObjectForLogging(msg.properties.headers) - ) ?? "", - }); + if (consumeAck.ignoreMsg(msg, ch)) { + logger.debug(TAG, 'Ignoring message', { mdc: amqpToMDC(msg) }) - if (consumeAck.ignoreMsg(msg, ch)) { - logger.debug("Ignoring message"); + return + } - return; - } + if (_.isNil(msg.properties.headers)) { + logger.warn( + TAG, + `Received message with no headers on "${msg.fields.exchange}". Assigning empty object.`, + { + evt: amqpHelper.safeAmqpObjectForLogging(msg), + }, + ) - if (_.isNil(msg.properties.headers)) { - logger.warn("Received message with no headers. Assigning empty object."); - msg.properties.headers = {}; - } + msg.properties.headers = {} + } - const handler = resolveMessageHandler(msg); + const handler = resolveMessageHandler(msg) - try { - const result: unknown = await handler.callback(msg); - consumeAck.safeAckMsg(msg, ch, "ack"); + try { + const result: unknown = await handler.callback(msg) + consumeAck.safeAckMsg(msg, ch, 'ack') - if (handler.kind === "WITH_REPLY" && result !== undefined) { - await publish - .publishReply( - msg, - >( - handler.replyMessageClass - ), - result, - ) - .catch((e) => { - logger.error( - "Publish reply failed", - e, - errors.enhancedDetails({ result }, e), - ); - }); - } - } catch (e) { - await consumeError.handleConsumeError( - e, - handler, - msgMeta, - ch, - msg, - ); - } - }; + if (handler.kind === 'WITH_REPLY' && result !== undefined) { + await publish + .publishReply( + msg, + >( + handler.replyMessageClass + ), + result, + ) + .catch((err) => { + logger.error(TAG, 'Publish reply failed', { + error: errors.enhancedDetails({ result }, err), + mdc: amqpToMDC(msg), + }) + }) + } + } catch (err) { + await consumeError.handleConsumeError( + errors.asError(err), + handler, + msgMeta, + ch, + msg, + ) + } + } } diff --git a/src/lib/consume.retry.ts b/src/lib/consume.retry.ts index 6f083c3..ea6b874 100644 --- a/src/lib/consume.retry.ts +++ b/src/lib/consume.retry.ts @@ -2,64 +2,67 @@ * Retry is managed by a dedicted manager service, feature is driven * through special headers attached to the message. */ -import * as amqplib from "amqplib"; -import { getLogger } from "../logger"; -import { cloneAmqpMsgProperties, getTemporaryChannel } from "./amqp.helper"; -import { connection } from "./connection"; -import { MANAGED_EXCHANGES, MESSAGE_HEADERS } from "./constants"; -import * as errors from "./errors"; -import flags from "./flags"; -import * as messageI from "./message.interfaces"; -import * as messageHandlerI from "./message_handler.interfaces"; +import type * as amqplib from 'amqplib' +import logger from '../logger' +import { cloneAmqpMsgProperties, getTemporaryChannel } from './amqp.helper' +import { connection } from './connection' +import { MANAGED_EXCHANGES, MESSAGE_HEADERS } from './constants' +import * as errors from './errors' +import flags from './flags' +import { amqpToMDC } from './mdc' +import type * as messageI from './message.interfaces' +import type * as messageHandlerI from './message_handler.interfaces' -const { DEAD_LETTER, RETRY } = MANAGED_EXCHANGES; +const { DEAD_LETTER, RETRY } = MANAGED_EXCHANGES -const logger = getLogger("Iris:Consumer:RetryEnqueue"); +const TAG = 'Iris:Consumer:RetryEnqueue' export async function enqueueWithBackoff( - msg: amqplib.ConsumeMessage, - handler: messageHandlerI.ProcessedMessageHandlerMetadataI, - msgMeta: messageI.ProcessedMessageConfigI, - error: Error, + msg: amqplib.ConsumeMessage, + handler: messageHandlerI.ProcessedMessageHandlerMetadataI, + msgMeta: messageI.ProcessedMessageConfigI, + error: Error, ): Promise { - if (flags.DISABLE_RETRY) { - return false; - } + if (flags.DISABLE_RETRY) { + return false + } - logger.errorDetails("Publishing to retry exchange"); + logger.error(TAG, 'Publishing to retry exchange', { + mdc: amqpToMDC(msg), + }) - const channel = await getTemporaryChannel("retry"); - const msgProperties = cloneAmqpMsgProperties(msg); - const { headers } = msgProperties; + const channel = await getTemporaryChannel('retry') + const msgProperties = cloneAmqpMsgProperties(msg) + const { headers } = msgProperties - headers[MESSAGE_HEADERS.REQUEUE.ORIGINAL_EXCHANGE] = msg.fields.exchange; - headers[MESSAGE_HEADERS.REQUEUE.ORIGINAL_ROUTING_KEY] = msg.fields.routingKey; - headers[MESSAGE_HEADERS.REQUEUE.ORIGINAL_QUEUE] = - handler.processedConfig.queueName; - headers[MESSAGE_HEADERS.REQUEUE.MAX_RETRIES] = - msgMeta.maxRetry ?? connection.getConfig().maxMessageRetryCount; - headers[MESSAGE_HEADERS.REQUEUE.NOTIFY_CLIENT] = errors.shouldNotifyClient( - error, - msg, - ); - headers[MESSAGE_HEADERS.MESSAGE.SERVER_TIMESTAMP] = Date.now(); + headers[MESSAGE_HEADERS.REQUEUE.ORIGINAL_EXCHANGE] = msg.fields.exchange + headers[MESSAGE_HEADERS.REQUEUE.ORIGINAL_ROUTING_KEY] = msg.fields.routingKey + headers[MESSAGE_HEADERS.REQUEUE.ORIGINAL_QUEUE] = + handler.processedConfig.queueName + headers[MESSAGE_HEADERS.REQUEUE.MAX_RETRIES] = + msgMeta.maxRetry ?? connection.getConfig().maxMessageRetryCount + headers[MESSAGE_HEADERS.REQUEUE.NOTIFY_CLIENT] = errors.shouldNotifyClient( + error, + msg, + ) + headers[MESSAGE_HEADERS.MESSAGE.SERVER_TIMESTAMP] = Date.now() - headers[MESSAGE_HEADERS.REQUEUE.ERROR_CODE] = error.constructor.name; - headers[MESSAGE_HEADERS.REQUEUE.ERROR_TYPE] = errors.getErrorType(error); - headers[MESSAGE_HEADERS.REQUEUE.ERROR_MESSAGE] = error.message; + headers[MESSAGE_HEADERS.REQUEUE.ERROR_CODE] = error.constructor.name + headers[MESSAGE_HEADERS.REQUEUE.ERROR_TYPE] = errors.getErrorType(error) + headers[MESSAGE_HEADERS.REQUEUE.ERROR_MESSAGE] = error.message - if (msgMeta.deadLetter !== "") { - const deadLetterRoutingKey = `${DEAD_LETTER.PREFIX}${handler.processedConfig.queueName}`; - headers[MESSAGE_HEADERS.QUEUE_DECLARATION.DEAD_LETTER_EXCHANGE] = - msgMeta.deadLetter; - headers[MESSAGE_HEADERS.QUEUE_DECLARATION.DEAD_LETTER_ROUTING_KEY] = - deadLetterRoutingKey; - } + if (msgMeta.deadLetter !== '') { + const deadLetterRoutingKey = `${DEAD_LETTER.PREFIX}${handler.processedConfig.queueName}` + headers[MESSAGE_HEADERS.QUEUE_DECLARATION.DEAD_LETTER_EXCHANGE] = + msgMeta.deadLetter + headers[MESSAGE_HEADERS.QUEUE_DECLARATION.DEAD_LETTER_ROUTING_KEY] = + deadLetterRoutingKey + } - return channel.publish( - RETRY.EXCHANGE, - RETRY.ROUTING_KEY, - Buffer.from(msg.content), - msgProperties, - ); + return channel.publish( + RETRY.EXCHANGE, + RETRY.ROUTING_KEY, + Buffer.from(msg.content), + msgProperties, + ) } diff --git a/src/lib/consume.ts b/src/lib/consume.ts index d7e2436..d8fcb8b 100644 --- a/src/lib/consume.ts +++ b/src/lib/consume.ts @@ -1,137 +1,138 @@ -import * as amqplib from "amqplib"; -import { getLogger } from "../logger"; -import * as amqpHelper from "./amqp.helper"; -import * as consumeHandle from "./consume.handle"; -import * as helper from "./helper"; -import * as message from "./message"; -import * as messageHandler from "./message_handler"; +import type * as amqplib from 'amqplib' +import logger from '../logger' +import * as amqpHelper from './amqp.helper' +import * as consumeHandle from './consume.handle' +import * as helper from './helper' +import * as message from './message' +import * as messageHandler from './message_handler' export type ConsumeHanderT = ( - msg: amqplib.ConsumeMessage | null, -) => Promise; + msg: amqplib.ConsumeMessage | null, +) => Promise type RegisterQueueConsumerT = { - handler: messageHandler.ProcessedMessageHandlerMetadataI; - consumerTag: string; - obtainChannel: () => Promise; - onChannelClose: () => void; -}; - -const logger = getLogger("Iris:Consumer"); -const queueConsumers: Record = {}; + handler: messageHandler.ProcessedMessageHandlerMetadataI + consumerTag: string + obtainChannel: () => Promise + onChannelClose: () => void +} + +const TAG = 'Iris:Consumer' + +const queueConsumers: Record = {} const frontendMessageHandlers: Record< - string, - messageHandler.ProcessedMessageHandlerMetadataI | undefined -> = {}; + string, + messageHandler.ProcessedMessageHandlerMetadataI | undefined +> = {} export async function registerConsumer( - handler: messageHandler.ProcessedMessageHandlerMetadataI, - msgMeta: message.ProcessedMessageMetadataI, - obtainChannel: () => Promise, - onChannelClose: () => void, + handler: messageHandler.ProcessedMessageHandlerMetadataI, + msgMeta: message.ProcessedMessageMetadataI, + obtainChannel: () => Promise, + onChannelClose: () => void, ): Promise { - const isFrontend = message.isFrontend(msgMeta); - const { exchange, queueName } = handler.processedConfig; - const consumerTag = getConsumerTag(msgMeta, handler); - const frontendHandlerLookupKey = getFrontendHandlerLookupKey( - msgMeta.processedConfig.routingKey, - ); - const queueToConsume = isFrontend - ? amqpHelper.getFrontendQueueName() - : queueName; - - const consumerRegistered = await registerQueueConsumerIfMissing({ - handler, - consumerTag, - obtainChannel, - onChannelClose, - }); - - if (isFrontend) { - frontendMessageHandlers[frontendHandlerLookupKey] = handler; - } - - if (!consumerRegistered) { - logger.debug("Already consuming queue", { queueName, exchange }); - - return; - } - - const channel = await obtainChannel(); - const resolveMessageHandler = isFrontend - ? getFrontendHandler - : (): messageHandler.ProcessedMessageHandlerMetadataI => handler; - - await channel.consume( - queueToConsume, - consumeHandle.getMessageHandler({ - obtainChannel, - onChannelClose, - msgMeta, - queueName, - resolveMessageHandler, - }), - { consumerTag }, - ); + const isFrontend = message.isFrontend(msgMeta) + const { exchange, queueName } = handler.processedConfig + const consumerTag = getConsumerTag(msgMeta, handler) + const frontendHandlerLookupKey = getFrontendHandlerLookupKey( + msgMeta.processedConfig.routingKey, + ) + const queueToConsume = isFrontend + ? amqpHelper.getFrontendQueueName() + : queueName + + const consumerRegistered = await registerQueueConsumerIfMissing({ + handler, + consumerTag, + obtainChannel, + onChannelClose, + }) + + if (isFrontend) { + frontendMessageHandlers[frontendHandlerLookupKey] = handler + } + + if (!consumerRegistered) { + logger.debug(TAG, 'Already consuming queue', { queueName, exchange }) + + return + } + + const channel = await obtainChannel() + const resolveMessageHandler = isFrontend + ? getFrontendHandler + : (): messageHandler.ProcessedMessageHandlerMetadataI => handler + + await channel.consume( + queueToConsume, + consumeHandle.getMessageHandler({ + obtainChannel, + onChannelClose, + msgMeta, + queueName, + resolveMessageHandler, + }), + { consumerTag }, + ) } async function registerQueueConsumerIfMissing({ - consumerTag, - handler, - obtainChannel, - onChannelClose, + consumerTag, + handler, + obtainChannel, + onChannelClose, }: RegisterQueueConsumerT): Promise { - const { queueName, exchange, bindingKeys } = handler.processedConfig; - - if (queueConsumers[consumerTag]) { - return false; - } - - queueConsumers[consumerTag] = true; - - const cleanup = (tag: string) => (): void => { - logger.warn(`Unregister queue consumer for (channel ${tag})`, { - queueName, - exchange, - bindingKeys, - consumerTag, - }); - queueConsumers[consumerTag] = false; - }; - - const channel = await obtainChannel(); - channel.once("close", cleanup("closed")); - channel.once("iris:reinit", cleanup("reinit")); - channel.once("close", onChannelClose); - - return true; + const { queueName, exchange, bindingKeys } = handler.processedConfig + + if (queueConsumers[consumerTag]) { + return false + } + + queueConsumers[consumerTag] = true + + const cleanup = (tag: string) => (): void => { + logger.debug(TAG, `Unregister queue consumer for (channel ${tag})`, { + queueName, + exchange, + bindingKeys, + consumerTag, + }) + queueConsumers[consumerTag] = false + } + + const channel = await obtainChannel() + channel.once('close', cleanup('closed')) + channel.once('iris:reinit', cleanup('reinit')) + channel.once('close', onChannelClose) + + return true } function getFrontendHandler( - msg: amqplib.ConsumeMessage, + msg: amqplib.ConsumeMessage, ): messageHandler.ProcessedMessageHandlerMetadataI { - return ( - frontendMessageHandlers[getFrontendHandlerLookupKey(msg.fields.routingKey)] - ); + return ( + frontendMessageHandlers[getFrontendHandlerLookupKey(msg.fields.routingKey)] + ) } export function getConsumerTag( - msgMeta: message.ProcessedMessageMetadataI, - handler: messageHandler.ProcessedMessageHandlerMetadataI, + msgMeta: message.ProcessedMessageMetadataI, + handler: messageHandler.ProcessedMessageHandlerMetadataI, ): string { - const { queueName, messageDeliveryMode } = handler.processedConfig; - const queueTag = message.isFrontend(msgMeta) - ? amqpHelper.getFrontendQueueName() - : queueName; - const delivery = - messageDeliveryMode === - messageHandler.MessageDeliveryMode.PER_SERVICE_INSTANCE - ? `(${messageDeliveryMode})` - : ""; - - return `${helper.getServiceName()}@${helper.getHostName()}#${queueTag}${delivery}`; + const { queueName, messageDeliveryMode } = handler.processedConfig + const queueTag = message.isFrontend(msgMeta) + ? amqpHelper.getFrontendQueueName() + : queueName + const delivery = + messageDeliveryMode === + messageHandler.MessageDeliveryMode.PER_SERVICE_INSTANCE + ? `(${messageDeliveryMode})` + : '' + + return `${helper.getServiceName()}@${helper.getHostName()}#${queueTag}${delivery}` } function getFrontendHandlerLookupKey(routingKey: string): string { - return `fontend_${routingKey}`; + return `fontend_${routingKey}` } diff --git a/src/lib/errors.ts b/src/lib/errors.ts index a721c06..4538561 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -1,77 +1,77 @@ -import * as amqplib from "amqplib"; -import { ClassConstructor } from "class-transformer"; -import * as classValidator from "class-validator"; -import { hasClientContext } from "./amqp.helper"; +import type * as amqplib from 'amqplib' +import type { ClassConstructor } from 'class-transformer' +import type * as classValidator from 'class-validator' +import { hasClientContext } from './amqp.helper' -export const UnauthorizedError = class UnauthorizedException extends Error {}; -export const ForbiddenError = class ForbiddenException extends Error {}; +export const UnauthorizedError = class UnauthorizedException extends Error {} +export const ForbiddenError = class ForbiddenException extends Error {} export interface CustomErrorI { - errorClass: ClassConstructor; - errorType: ErrorTypeE; - // when registring custom errors, this flag - // can be used to override `notifyClient` flag - // on error itself. - alwaysNotifyClient?: true; + errorClass: ClassConstructor + errorType: ErrorTypeE + // when registring custom errors, this flag + // can be used to override `notifyClient` flag + // on error itself. + alwaysNotifyClient?: true } -const REJECTABLE_ERRORS: CustomErrorI[] = []; +const REJECTABLE_ERRORS: CustomErrorI[] = [] export interface ErrorMessageI { - errorType: ErrorTypeE; - code: string; - message: string; + errorType: ErrorTypeE + code: string + message: string } export enum ErrorTypeE { - // security - FORBIDDEN = "FORBIDDEN", - UNAUTHORIZED = "UNAUTHORIZED", - AUTHORIZATION_FAILED = "AUTHORIZATION_FAILED", - // client - BAD_REQUEST = "BAD_REQUEST", - NOT_FOUND = "NOT_FOUND", - // server - INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR", + // security + FORBIDDEN = 'FORBIDDEN', + UNAUTHORIZED = 'UNAUTHORIZED', + AUTHORIZATION_FAILED = 'AUTHORIZATION_FAILED', + // client + BAD_REQUEST = 'BAD_REQUEST', + NOT_FOUND = 'NOT_FOUND', + // server + INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', } export abstract class MsgError extends Error { - errorType: ErrorTypeE = ErrorTypeE.INTERNAL_SERVER_ERROR; - - /** - * Whether error should be propagated back to the client. - * If not set, IRIS will do this when client's context is - * available. - */ - notifyClient?: boolean = undefined; - - constructor(msg: string); - constructor(msg: string, notifyClient: boolean); - constructor(msg: string, notifyClient?: boolean) { - super(msg); - if (notifyClient !== undefined) { - this.notifyClient = notifyClient; - } - } - - /** - * Whether error should be propagated back to the client. - * If not set, IRIS will do this when client's context is - * available. - */ - public setNotifyClient(notifyClient: boolean): MsgError { - this.notifyClient = notifyClient; - - return this; - } - - public trhow(): never { - throw this; - } - - public getMessage(): string { - return this.message; - } + errorType: ErrorTypeE = ErrorTypeE.INTERNAL_SERVER_ERROR + + /** + * Whether error should be propagated back to the client. + * If not set, IRIS will do this when client's context is + * available. + */ + notifyClient?: boolean = undefined + + constructor(msg: string) + constructor(msg: string, notifyClient: boolean) + constructor(msg: string, notifyClient?: boolean) { + super(msg) + if (notifyClient !== undefined) { + this.notifyClient = notifyClient + } + } + + /** + * Whether error should be propagated back to the client. + * If not set, IRIS will do this when client's context is + * available. + */ + public setNotifyClient(notifyClient: boolean): MsgError { + this.notifyClient = notifyClient + + return this + } + + public trhow(): never { + throw this + } + + public getMessage(): string { + return this.message + } } /** @@ -81,121 +81,128 @@ export abstract class MsgError extends Error { */ export class RejectMsgError extends MsgError { - errorType: ErrorTypeE = ErrorTypeE.BAD_REQUEST; + errorType: ErrorTypeE = ErrorTypeE.BAD_REQUEST } export class InvalidObjectConverionError extends RejectMsgError { - validationErrors: classValidator.ValidationError[]; + validationErrors: classValidator.ValidationError[] - constructor(errorDetails: classValidator.ValidationError[]) { - super("InvalidObjectCoversion"); - this.validationErrors = errorDetails; - } + constructor(errorDetails: classValidator.ValidationError[]) { + super('InvalidObjectCoversion') + this.validationErrors = errorDetails + } - public getMessage(): string { - return JSON.stringify(this.validationErrors); - } + public getMessage(): string { + return JSON.stringify(this.validationErrors) + } } export function enhancedDetails(details: T, error: Error): T { - let detailsEnhanced = { ...details }; - if (error instanceof InvalidObjectConverionError) { - detailsEnhanced = { - ...detailsEnhanced, - validation: error.validationErrors, - }; - } else if (error instanceof Error) { - detailsEnhanced = { - ...details, - err: JSON.stringify(error, Object.getOwnPropertyNames(error)), - }; - } else { - detailsEnhanced = { ...details, err: error }; - } - - return detailsEnhanced; + let detailsEnhanced = { ...details } + if (error instanceof InvalidObjectConverionError) { + detailsEnhanced = { + ...detailsEnhanced, + validation: error.validationErrors, + } + } else if (error instanceof Error) { + detailsEnhanced = { + ...details, + err: JSON.stringify(error, Object.getOwnPropertyNames(error)), + } + } else { + detailsEnhanced = { ...details, err: error } + } + + return detailsEnhanced } export function getErrorType(error: Error): ErrorTypeE { - if (error instanceof MsgError) { - return error.errorType; - } - - if (error instanceof UnauthorizedError) { - return ErrorTypeE.UNAUTHORIZED; - } - if (error instanceof ForbiddenError) { - return ErrorTypeE.FORBIDDEN; - } - - const asRejectable = getIfRejectableError(error); - if (asRejectable !== undefined) { - return asRejectable.errorType; - } - - return ErrorTypeE.INTERNAL_SERVER_ERROR; + if (error instanceof MsgError) { + return error.errorType + } + + if (error instanceof UnauthorizedError) { + return ErrorTypeE.UNAUTHORIZED + } + if (error instanceof ForbiddenError) { + return ErrorTypeE.FORBIDDEN + } + + const asRejectable = getIfRejectableError(error) + if (asRejectable !== undefined) { + return asRejectable.errorType + } + + return ErrorTypeE.INTERNAL_SERVER_ERROR } export function getErrorMessage(error: Error): ErrorMessageI { - const message = - error instanceof MsgError ? error.getMessage() : error.message; - - return { - errorType: getErrorType(error), - code: error.constructor.name, - message: message, - }; + const message = error instanceof MsgError ? error.getMessage() : error.message + + return { + errorType: getErrorType(error), + code: error.constructor.name, + message: message, + } } export function shouldNotifyClient( - error: Error, - msg: amqplib.ConsumeMessage, + error: Error, + msg: amqplib.ConsumeMessage, ): boolean { - const customRejectableError = getIfRejectableError(error); + const customRejectableError = getIfRejectableError(error) - if (customRejectableError?.alwaysNotifyClient === true) { - return true; - } + if (customRejectableError?.alwaysNotifyClient === true) { + return true + } - const explicit = error instanceof MsgError ? error.notifyClient : undefined; + const explicit = error instanceof MsgError ? error.notifyClient : undefined - return explicit ?? hasClientContext(msg); + return explicit ?? hasClientContext(msg) } export function isRejectableError(error: Error): boolean { - const isRejectable = getIfRejectableError(error) !== undefined; + const isRejectable = getIfRejectableError(error) !== undefined - if (isRejectable) { - return true; - } + if (isRejectable) { + return true + } - if (error instanceof MsgError) { - const errorType = error.errorType; + if (error instanceof MsgError) { + const errorType = error.errorType - return ( - errorType === ErrorTypeE.AUTHORIZATION_FAILED || - errorType === ErrorTypeE.UNAUTHORIZED || - errorType === ErrorTypeE.FORBIDDEN - ); - } + return ( + errorType === ErrorTypeE.AUTHORIZATION_FAILED || + errorType === ErrorTypeE.UNAUTHORIZED || + errorType === ErrorTypeE.FORBIDDEN + ) + } - return false; + return false } export function registerRejectableErrors(errorClasses: CustomErrorI[]): void { - errorClasses.reverse().forEach((errorClass) => { - REJECTABLE_ERRORS.unshift(errorClass); - }); + for (const errorClass of errorClasses.reverse()) { + REJECTABLE_ERRORS.unshift(errorClass) + } +} + +export function asError(err: any): Error { + if (err instanceof Error) { + return err + } + + return new Error(err) } function getIfRejectableError(error: Error): CustomErrorI | undefined { - return REJECTABLE_ERRORS.find(({ errorClass }) => { - return error instanceof errorClass; - }); + return REJECTABLE_ERRORS.find(({ errorClass }) => { + return error instanceof errorClass + }) } registerRejectableErrors([ - { errorClass: UnauthorizedError, errorType: ErrorTypeE.UNAUTHORIZED }, - { errorClass: ForbiddenError, errorType: ErrorTypeE.FORBIDDEN }, - { errorClass: RejectMsgError, errorType: ErrorTypeE.BAD_REQUEST }, -]); + { errorClass: UnauthorizedError, errorType: ErrorTypeE.UNAUTHORIZED }, + { errorClass: ForbiddenError, errorType: ErrorTypeE.FORBIDDEN }, + { errorClass: RejectMsgError, errorType: ErrorTypeE.BAD_REQUEST }, +]) diff --git a/src/lib/feat.management.ts b/src/lib/feat.management.ts index 238e890..b8eee89 100644 --- a/src/lib/feat.management.ts +++ b/src/lib/feat.management.ts @@ -1,52 +1,52 @@ -import * as amqpHelper from "./amqp.helper"; +import * as amqpHelper from './amqp.helper' /** * Setup exhanges managed and owned by dedicated services. * Only use for tests. */ import { - MANAGED_EXCHANGES, - ManagedExchangeI, - ManagedExchangesE, -} from "./constants"; + MANAGED_EXCHANGES, + type ManagedExchangeI, + ManagedExchangesE, +} from './constants' // Subscription Message classes are already defined elsewhere type ReservedManagedExchangesE = Exclude< - ManagedExchangesE, - | ManagedExchangesE.SUBSCRIPTION - | ManagedExchangesE.SNAPSHOT_REQUESTED - | ManagedExchangesE.SUBSCRIBE_INTERNAL - | ManagedExchangesE.FRONTEND ->; + ManagedExchangesE, + | ManagedExchangesE.SUBSCRIPTION + | ManagedExchangesE.SNAPSHOT_REQUESTED + | ManagedExchangesE.SUBSCRIBE_INTERNAL + | ManagedExchangesE.FRONTEND +> const reservedExchangesToRegister: Record< - ReservedManagedExchangesE, - ManagedExchangeI + ReservedManagedExchangesE, + ManagedExchangeI > = { - [ManagedExchangesE.DEAD_LETTER]: MANAGED_EXCHANGES.DEAD_LETTER, - [ManagedExchangesE.ERROR]: MANAGED_EXCHANGES.ERROR, - [ManagedExchangesE.RETRY]: MANAGED_EXCHANGES.RETRY, - [ManagedExchangesE.BROADCAST]: MANAGED_EXCHANGES.BROADCAST, - [ManagedExchangesE.SESSION]: MANAGED_EXCHANGES.SESSION, - [ManagedExchangesE.USER]: MANAGED_EXCHANGES.USER, -}; + [ManagedExchangesE.DEAD_LETTER]: MANAGED_EXCHANGES.DEAD_LETTER, + [ManagedExchangesE.ERROR]: MANAGED_EXCHANGES.ERROR, + [ManagedExchangesE.RETRY]: MANAGED_EXCHANGES.RETRY, + [ManagedExchangesE.BROADCAST]: MANAGED_EXCHANGES.BROADCAST, + [ManagedExchangesE.SESSION]: MANAGED_EXCHANGES.SESSION, + [ManagedExchangesE.USER]: MANAGED_EXCHANGES.USER, +} export async function registerFrontendExchange(): Promise { - const { EXCHANGE, EXCHANGE_TYPE, EXCHANGE_OPTIONS } = - MANAGED_EXCHANGES.FRONTEND; - await amqpHelper.assertExchange(EXCHANGE, EXCHANGE_TYPE, EXCHANGE_OPTIONS); + const { EXCHANGE, EXCHANGE_TYPE, EXCHANGE_OPTIONS } = + MANAGED_EXCHANGES.FRONTEND + await amqpHelper.assertExchange(EXCHANGE, EXCHANGE_TYPE, EXCHANGE_OPTIONS) } export async function registerReservedManagedMessages(): Promise { - await Promise.all( - Object.values(reservedExchangesToRegister).map( - async ({ EXCHANGE, EXCHANGE_TYPE, EXCHANGE_OPTIONS }) => - amqpHelper.assertExchange(EXCHANGE, EXCHANGE_TYPE, EXCHANGE_OPTIONS), - ), - ); + await Promise.all( + Object.values(reservedExchangesToRegister).map( + async ({ EXCHANGE, EXCHANGE_TYPE, EXCHANGE_OPTIONS }) => + amqpHelper.assertExchange(EXCHANGE, EXCHANGE_TYPE, EXCHANGE_OPTIONS), + ), + ) } export function getReservedManagedExchangeNames(): string[] { - return Object.values(reservedExchangesToRegister).map( - ({ EXCHANGE }) => EXCHANGE, - ); + return Object.values(reservedExchangesToRegister).map( + ({ EXCHANGE }) => EXCHANGE, + ) } diff --git a/src/lib/flags.ts b/src/lib/flags.ts index cd049ac..320a888 100644 --- a/src/lib/flags.ts +++ b/src/lib/flags.ts @@ -1,45 +1,55 @@ -import * as helper from "./helper"; +import * as helper from './helper' -const IRIS_DISABLE_RETRY = "IRIS_DISABLE_RETRY"; +const IRIS_DISABLE_RETRY = 'IRIS_DISABLE_RETRY' const IRIS_DISABLE_MESSAGE_CONSUME_VALIDATION = - "DISABLE_MESSAGE_CONSUME_VALIDATION"; + 'DISABLE_MESSAGE_CONSUME_VALIDATION' const IRIS_DISABLE_MESSAGE_PRODUCE_VALIDATION = - "DISABLE_MESSAGE_PRODUCE_VALIDATION"; + 'DISABLE_MESSAGE_PRODUCE_VALIDATION' // only for internal use during tests -let ALLOW_USING_RESERVED_NAMES = false; +let ALLOW_USING_RESERVED_NAMES = false + +// do not create exchanges for messages +// where scope is not `internal` +let ASSERT_NON_INTERNAL_EXCHANGES = false const flags = { - get DISABLE_RETRY(): boolean { - return helper.isEnvTrue(process.env[IRIS_DISABLE_RETRY]); - }, - set DISABLE_RETRY(flag: boolean) { - process.env[IRIS_DISABLE_RETRY] = helper.getEnvBoolFlag(flag); - }, - get DISABLE_MESSAGE_CONSUME_VALIDATION(): boolean { - return helper.isEnvTrue( - process.env[IRIS_DISABLE_MESSAGE_CONSUME_VALIDATION], - ); - }, - set DISABLE_MESSAGE_CONSUME_VALIDATION(flag: boolean) { - process.env[IRIS_DISABLE_MESSAGE_CONSUME_VALIDATION] = - helper.getEnvBoolFlag(flag); - }, - get DISABLE_MESSAGE_PRODUCE_VALIDATION(): boolean { - return helper.isEnvTrue( - process.env[IRIS_DISABLE_MESSAGE_PRODUCE_VALIDATION], - ); - }, - set DISABLE_MESSAGE_PRODUCE_VALIDATION(flag: boolean) { - process.env[IRIS_DISABLE_MESSAGE_PRODUCE_VALIDATION] = - helper.getEnvBoolFlag(flag); - }, - get ALLOW_USING_RESERVED_NAMES(): boolean { - return ALLOW_USING_RESERVED_NAMES; - }, - set ALLOW_USING_RESERVED_NAMES(flag: boolean) { - ALLOW_USING_RESERVED_NAMES = flag; - }, -}; + get DISABLE_RETRY(): boolean { + return helper.isEnvTrue(process.env[IRIS_DISABLE_RETRY]) + }, + set DISABLE_RETRY(flag: boolean) { + process.env[IRIS_DISABLE_RETRY] = helper.getEnvBoolFlag(flag) + }, + get DISABLE_MESSAGE_CONSUME_VALIDATION(): boolean { + return helper.isEnvTrue( + process.env[IRIS_DISABLE_MESSAGE_CONSUME_VALIDATION], + ) + }, + set DISABLE_MESSAGE_CONSUME_VALIDATION(flag: boolean) { + process.env[IRIS_DISABLE_MESSAGE_CONSUME_VALIDATION] = + helper.getEnvBoolFlag(flag) + }, + get DISABLE_MESSAGE_PRODUCE_VALIDATION(): boolean { + return helper.isEnvTrue( + process.env[IRIS_DISABLE_MESSAGE_PRODUCE_VALIDATION], + ) + }, + set DISABLE_MESSAGE_PRODUCE_VALIDATION(flag: boolean) { + process.env[IRIS_DISABLE_MESSAGE_PRODUCE_VALIDATION] = + helper.getEnvBoolFlag(flag) + }, + get ALLOW_USING_RESERVED_NAMES(): boolean { + return ALLOW_USING_RESERVED_NAMES + }, + set ALLOW_USING_RESERVED_NAMES(flag: boolean) { + ALLOW_USING_RESERVED_NAMES = flag + }, + get ASSERT_NON_INTERNAL_EXCHANGES(): boolean { + return ASSERT_NON_INTERNAL_EXCHANGES + }, + set ASSERT_NON_INTERNAL_EXCHANGES(flag: boolean) { + ASSERT_NON_INTERNAL_EXCHANGES = flag + }, +} -export default flags; +export default flags diff --git a/src/lib/helper.ts b/src/lib/helper.ts index 1ac7035..758ed26 100644 --- a/src/lib/helper.ts +++ b/src/lib/helper.ts @@ -1,46 +1,46 @@ -import * as os from "os"; -import _ from "lodash"; +import * as os from 'node:os' +import _ from 'lodash' -let serviceName = "{SERVICE_NAME}"; +let serviceName = '{SERVICE_NAME}' export function setServiceName(sName: string): void { - serviceName = _.kebabCase(sName); + serviceName = _.kebabCase(sName) } export function getServiceName(): string { - return serviceName; + return serviceName } export function getHostName(): string { - return os.hostname(); + return os.hostname() } export function getConsumerTag(postfixTag: string): string { - return `${getServiceName()}@${getHostName()}#${postfixTag}`; + return `${getServiceName()}@${getHostName()}#${postfixTag}` } export function isEnvTrue(value?: string): boolean { - return value === undefined - ? false - : ["1", "true"].includes(`${value}`.toLowerCase()); + return value === undefined + ? false + : ['1', 'true'].includes(`${value}`.toLowerCase()) } export function getEnvBoolFlag(flag: boolean): string { - return flag ? "true" : "false"; + return flag ? 'true' : 'false' } export function nonemptyString(arg: unknown): arg is string { - return _.isString(arg) && !_.isEmpty(arg); + return _.isString(arg) && !_.isEmpty(arg) } export function getTargetConstructor(target: Object | Function): Function { - return target instanceof Function ? target : target.constructor; + return target instanceof Function ? target : target.constructor } export function classIsSameOrSubclassOf( - classToCheck: Function, - classToCompareTo: Function, + classToCheck: Function, + classToCompareTo: Function, ): boolean { - return ( - classToCheck === classToCompareTo || - classToCheck.prototype instanceof classToCompareTo - ); + return ( + classToCheck === classToCompareTo || + classToCheck.prototype instanceof classToCompareTo + ) } diff --git a/src/lib/mdc.ts b/src/lib/mdc.ts new file mode 100644 index 0000000..917f4bc --- /dev/null +++ b/src/lib/mdc.ts @@ -0,0 +1,69 @@ +/** + * Since we don't have MDC like Java, we need + * to have a way to provide same information, but + * via param decorators and utilities. + */ + +import _ from 'lodash' +import { MDC_PROPERTIES as MDC_PROPS, MESSAGE_HEADERS } from './constants' +import { AmqpMessage } from './message_handler.param.decorator' +import { MDC_CLASS, SetMetadata } from './storage' + +const { MESSAGE: MH } = MESSAGE_HEADERS + +export type MdcI = { + sessionId?: string + userId?: string + clientTraceId?: string + correlationId?: string + eventType?: string + clientVersion?: string +} + +export class MDC implements MdcI { + sessionId?: string + userId?: string + clientTraceId?: string + correlationId?: string + eventType?: string + clientVersion?: string +} + +SetMetadata(MDC_CLASS, true)(MDC) + +export function amqpToMDC(amqpMessage: Pick) { + const ref: MdcI = {} + + setFromHeader(amqpMessage, ref, MH.SESSION_ID, MDC_PROPS.SESSION_ID) + setFromHeader(amqpMessage, ref, MH.USER_ID, MDC_PROPS.USER_ID) + setFromHeader(amqpMessage, ref, MH.CLIENT_TRACE_ID, MDC_PROPS.CLIENT_TRACE_ID) + setFromHeader(amqpMessage, ref, MH.CORRELATION_ID, MDC_PROPS.CORRELATION_ID) + setFromHeader(amqpMessage, ref, MH.EVENT_TYPE, MDC_PROPS.EVENT_TYPE) + setFromHeader(amqpMessage, ref, MH.CLIENT_VERSION, MDC_PROPS.CLIENT_VERSION) + + return Object.assign(new MDC(), ref) +} + +export const isMDCClass = (target: unknown): boolean => { + if (typeof target === 'object' && target !== null) { + if (target instanceof MDC) { + return true + } + } else if (typeof target === 'function') { + return target === MDC || Reflect.getMetadata(MDC_CLASS, target) + } + + return false +} + +function setFromHeader( + amqpMessage: Pick, + ref: MdcI, + header: string, + key: string, +) { + const val = _.get(amqpMessage, `properties.headers.${header}`) + if (!_.isEmpty(val)) { + ref[key] = val + } +} diff --git a/src/lib/message.decorator.ts b/src/lib/message.decorator.ts index b4214b8..954d323 100644 --- a/src/lib/message.decorator.ts +++ b/src/lib/message.decorator.ts @@ -1,8 +1,8 @@ -import * as uuid from "uuid"; -import * as helper from "./helper"; -import * as interfaces from "./message.interfaces"; -import * as storage from "./storage"; -import * as validationI from "./validation.interfaces"; +import { randomUUID } from 'node:crypto' +import * as helper from './helper' +import type * as interfaces from './message.interfaces' +import * as storage from './storage' +import type * as validationI from './validation.interfaces' /** * AMQP exchange decorator. @@ -10,22 +10,22 @@ import * as validationI from "./validation.interfaces"; * Use class-validator decorators (eg. @IsString()) on class's properties for message automatic validation. */ export const Message = - ( - config: interfaces.MessageI, - validationOptions?: validationI.ValidationOptions, - ): ClassDecorator => - (target): void => { - const targetClassName = helper.getTargetConstructor(target).name; + ( + config: interfaces.MessageI, + validationOptions?: validationI.ValidationOptions, + ): ClassDecorator => + (target): void => { + const targetClassName = helper.getTargetConstructor(target).name - storage.registerMessage(target); - storage.SetMetadata( - storage.IRIS_MESSAGE, - { - uuid: uuid.v4(), - target, - targetClassName, - validation: validationOptions, - origDecoratorConfig: config, - }, - )(target); - }; + storage.registerMessage(target) + storage.SetMetadata( + storage.IRIS_MESSAGE, + { + uuid: randomUUID(), + target, + targetClassName, + validation: validationOptions, + origDecoratorConfig: config, + }, + )(target) + } diff --git a/src/lib/message.decorator_utils.ts b/src/lib/message.decorator_utils.ts index c14d66e..b80200b 100644 --- a/src/lib/message.decorator_utils.ts +++ b/src/lib/message.decorator_utils.ts @@ -1,89 +1,89 @@ -import _ from "lodash"; -import * as helper from "./helper"; -import * as interfaces from "./message.interfaces"; -import * as processMsg from "./message.process"; -import * as storage from "./storage"; -import * as validationI from "./validation.interfaces"; +import _ from 'lodash' +import * as helper from './helper' +import * as interfaces from './message.interfaces' +import * as processMsg from './message.process' +import * as storage from './storage' +import * as validationI from './validation.interfaces' export function isMessageDecoratedClass(messageClass: Object): boolean { - return Reflect.getMetadata(storage.IRIS_MESSAGE, messageClass) !== undefined; + return Reflect.getMetadata(storage.IRIS_MESSAGE, messageClass) !== undefined } export function getMessageDecoratedClass( - messageClass: Object, + messageClass: Object, ): interfaces.MessageMetadataI { - const msgMeta = ( - Reflect.getMetadata(storage.IRIS_MESSAGE, messageClass) - ); - if (msgMeta === undefined) { - throw new validationI.ValidationError( - "NO_SUCH_MESSAGE", - "Class is not decorated usig @Message()", - { - targetClassName: helper.getTargetConstructor(messageClass).name, - }, - ); - } + const msgMeta = ( + Reflect.getMetadata(storage.IRIS_MESSAGE, messageClass) + ) + if (msgMeta === undefined) { + throw new validationI.ValidationError( + 'NO_SUCH_MESSAGE', + 'Class is not decorated usig @Message()', + { + targetClassName: helper.getTargetConstructor(messageClass).name, + }, + ) + } - return msgMeta; + return msgMeta } export function getProcessedMessageDecoratedClass( - messageClass: Object, + messageClass: Object, ): interfaces.ProcessedMessageMetadataI { - const msgMeta = getMessageDecoratedClass(messageClass); - const processedConfig = processMsg.processAndValidateConfig( - msgMeta.origDecoratorConfig, - msgMeta.target, - ); + const msgMeta = getMessageDecoratedClass(messageClass) + const processedConfig = processMsg.processAndValidateConfig( + msgMeta.origDecoratorConfig, + msgMeta.target, + ) - validateMessageIsUnique(msgMeta); + validateMessageIsUnique(msgMeta) - return { ...msgMeta, processedConfig }; + return { ...msgMeta, processedConfig } } export function isFrontend(arg: { - processedConfig: Pick; + processedConfig: Pick }): boolean { - const { scope } = arg.processedConfig; + const { scope } = arg.processedConfig - return scope === interfaces.Scope.FRONTEND; + return scope === interfaces.Scope.FRONTEND } export function collectProcessedMessages(): interfaces.ProcessedMessageMetadataI[] { - return storage.getMessageStore().map(getProcessedMessageDecoratedClass); + return storage.getMessageStore().map(getProcessedMessageDecoratedClass) } function validateMessageIsUnique(msgMeta: interfaces.MessageMetadataI): void { - const { target } = msgMeta; - const msgWithSameName = storage - .getMessageStore() - .map(getMessageDecoratedClass) - .find( - (msg) => - msg.target !== msgMeta.target && - exchangeNameDistinct(msg) === exchangeNameDistinct(msgMeta), - ); + const { target } = msgMeta + const msgWithSameName = storage + .getMessageStore() + .map(getMessageDecoratedClass) + .find( + (msg) => + msg.target !== msgMeta.target && + exchangeNameDistinct(msg) === exchangeNameDistinct(msgMeta), + ) - if (msgWithSameName !== undefined) { - throw new validationI.ValidationError( - "MESSAGE_NAME_NOT_UNIQUE", - "Another class decorated with @Message() holds same `name`", - { - thisClassName: helper.getTargetConstructor(target).name, - otherClassName: helper.getTargetConstructor(msgWithSameName.target) - .name, - name: msgMeta.origDecoratorConfig.name, - }, - ); - } + if (msgWithSameName !== undefined) { + throw new validationI.ValidationError( + 'MESSAGE_NAME_NOT_UNIQUE', + 'Another class decorated with @Message() holds same `name`', + { + thisClassName: helper.getTargetConstructor(target).name, + otherClassName: helper.getTargetConstructor(msgWithSameName.target) + .name, + name: msgMeta.origDecoratorConfig.name, + }, + ) + } } function exchangeNameDistinct(arg: { - origDecoratorConfig: Pick; + origDecoratorConfig: Pick }): string { - return [ - arg.origDecoratorConfig.scope ?? "", - _.kebabCase(arg.origDecoratorConfig.name), - ].join("_"); + return [ + arg.origDecoratorConfig.scope ?? '', + _.kebabCase(arg.origDecoratorConfig.name), + ].join('_') } diff --git a/src/lib/message.interfaces.ts b/src/lib/message.interfaces.ts index 2ce7352..c19882f 100644 --- a/src/lib/message.interfaces.ts +++ b/src/lib/message.interfaces.ts @@ -1,92 +1,97 @@ -import * as amqplib from "amqplib"; -import * as validationI from "./validation.interfaces"; +import type * as amqplib from 'amqplib' +import type * as validationI from './validation.interfaces' export type AssertExchangeI = amqplib.Options.AssertExchange & { - durable: boolean; - autoDelete: boolean; -}; + durable: boolean + autoDelete: boolean +} export enum ExchangeType { - // bindingKey must equal routingKey - direct = "direct", - // bindingKey is a pattern containing `*` and/or `#` chars. - // If non are present then it's same as using `direct` - topic = "topic", - // bindingKey is ignored - fanout = "fanout", + // bindingKey must equal routingKey + direct = 'direct', + // bindingKey is a pattern containing `*` and/or `#` chars. + // If non are present then it's same as using `direct` + topic = 'topic', + // bindingKey is ignored + fanout = 'fanout', } export enum Scope { - /** - * Backend inter-service message - */ - INTERNAL = "INTERNAL", - /** - * Request message on websocket from client/frontend - */ - FRONTEND = "FRONTEND", - /** - * Message intended for user on all his sessions - */ - USER = "USER", - /** - * Message intended for user on exact session - */ - SESSION = "SESSION", - /** - * Message intended for all users on all sessions - */ - BROADCAST = "BROADCAST", + /** + * Backend inter-service message + */ + INTERNAL = 'INTERNAL', + /** + * Request message on websocket from client/frontend + */ + FRONTEND = 'FRONTEND', + /** + * Message intended for user on all his sessions + */ + USER = 'USER', + /** + * Message intended for user on exact session + */ + SESSION = 'SESSION', + /** + * Message intended for all users on all sessions + */ + BROADCAST = 'BROADCAST', } export interface MessageI { - /** - * Name of exchange - */ - name: string; - /** - * Defaults to `fanout` - */ - exchangeType?: ExchangeType; - routingKey?: string; - scope?: Scope; - ttl?: number; - deadLetter?: string; - /** - * Max times this message should be sent to retry queue - * before considered unhandled. Overrides default from - * connection configuration. - */ - maxRetry?: number; + /** + * Name of exchange + */ + name: string + /** + * Defaults to `fanout` + */ + exchangeType?: ExchangeType + routingKey?: string + scope?: Scope + ttl?: number + deadLetter?: string + /** + * Max times this message should be sent to retry queue + * before considered unhandled. Overrides default from + * connection configuration. + */ + maxRetry?: number } export interface MessageMetadataI { - uuid: string; - target: Object; - targetClassName: string; - validation?: validationI.ValidationOptions; - origDecoratorConfig: MessageI; + uuid: string + target: Object + targetClassName: string + validation?: validationI.ValidationOptions + origDecoratorConfig: MessageI } export interface ProcessedMessageMetadataI extends MessageMetadataI { - processedConfig: ProcessedMessageConfigI; + processedConfig: ProcessedMessageConfigI } -export interface ProcessedMessageConfigI extends Omit { - exchangeName: string; - /** - * Most SCOPEs publish to specific exchanges - */ - publishingExchangeName: string; - /** - * If set, must be used when publishing instead of - * routingKey - */ - publishingExchangeRoutingKey?: string; - exchangeType: ExchangeType; - routingKey: string; - scope: Scope; - deadLetter: string; - deadLetterIsCustom: boolean; - exchangeOptions: AssertExchangeI; +export interface ProcessedMessageConfigI extends Omit { + exchangeName: string + /** + * Most SCOPEs publish to specific exchanges + */ + publishingExchangeName: string + /** + * If set, must be used when publishing instead of + * routingKey + */ + publishingExchangeRoutingKey?: string + exchangeType: ExchangeType + routingKey: string + scope: Scope + deadLetter: string + deadLetterIsCustom: boolean + exchangeOptions: AssertExchangeI + /** + * SESSION, USER, BROADCAST and FRONTEND scope events + * do not need dedicaed exchanges + */ + doAssertExchange: boolean } diff --git a/src/lib/message.process.ts b/src/lib/message.process.ts index 6dc4a15..3817093 100644 --- a/src/lib/message.process.ts +++ b/src/lib/message.process.ts @@ -1,112 +1,118 @@ import { - MANAGED_EXCHANGES, - getExchangeDefaultsForExchangeName, -} from "./constants"; -import * as interfaces from "./message.interfaces"; -import * as validation from "./message.validation"; + MANAGED_EXCHANGES, + getExchangeDefaultsForExchangeName, +} from './constants' +import flags from './flags' +import * as interfaces from './message.interfaces' +import * as validation from './message.validation' -const { DEAD_LETTER, FRONTEND, USER, SESSION, BROADCAST } = MANAGED_EXCHANGES; +const { DEAD_LETTER, FRONTEND, USER, SESSION, BROADCAST } = MANAGED_EXCHANGES export function processAndValidateConfig( - config: interfaces.MessageI, - target: Object, + config: interfaces.MessageI, + target: Object, ): interfaces.ProcessedMessageConfigI { - validation.validateProcessedMessageConfig(config, target); - - const exchangeName = config.name; - - const { routingKey } = config; - const intermediate = { - ...getExchangeDefaultsForExchangeName(exchangeName), - ...config, - routingKey: routingKey ?? exchangeName, - ttl: getTTL(config), - deadLetter: getDeadLetter(config), - exchangeName, - }; - - const processedMessageConfig = { - deadLetterIsCustom: isDeadletterCustom(intermediate.deadLetter), - ...intermediate, - ...getPublishExchangeProps(intermediate), - }; - - return processedMessageConfig; + validation.validateProcessedMessageConfig(config, target) + + const exchangeName = config.name + + const { routingKey } = config + const intermediate = { + ...getExchangeDefaultsForExchangeName(exchangeName), + ...config, + routingKey: routingKey ?? exchangeName, + ttl: getTTL(config), + deadLetter: getDeadLetter(config), + exchangeName, + } + + const processedMessageConfig = { + deadLetterIsCustom: isDeadletterCustom(intermediate.deadLetter), + ...intermediate, + ...getPublishExchangeProps(intermediate), + } + + return processedMessageConfig } function getDeadLetter(config: interfaces.MessageI): string { - const { scope, deadLetter: dl } = config; - const deadLetter = typeof dl === "string" ? dl.trim() : undefined; + const { scope, deadLetter: dl } = config + const deadLetter = typeof dl === 'string' ? dl.trim() : undefined - if (deadLetter === "") { - return ""; - } + if (deadLetter === '') { + return '' + } - if (scope === interfaces.Scope.FRONTEND) { - return ""; - } + if (scope === interfaces.Scope.FRONTEND) { + return '' + } - if (typeof deadLetter !== "string") { - return DEAD_LETTER.QUEUE; - } + if (typeof deadLetter !== 'string') { + return DEAD_LETTER.QUEUE + } - if (!deadLetter.startsWith(DEAD_LETTER.PREFIX)) { - return `${DEAD_LETTER.PREFIX}${deadLetter}`; - } + if (!deadLetter.startsWith(DEAD_LETTER.PREFIX)) { + return `${DEAD_LETTER.PREFIX}${deadLetter}` + } - return deadLetter; + return deadLetter } function isDeadletterCustom(name: string): boolean { - return name !== "" && name !== DEAD_LETTER.QUEUE; + return name !== '' && name !== DEAD_LETTER.QUEUE } function getTTL(config: interfaces.MessageI): number | undefined { - const { ttl, scope } = config; + const { ttl, scope } = config - if (scope === interfaces.Scope.FRONTEND && ttl === undefined) { - return FRONTEND.TTL; - } + if (scope === interfaces.Scope.FRONTEND && ttl === undefined) { + return FRONTEND.TTL + } - if (ttl !== undefined && ttl < 0) { - return undefined; - } + if (ttl !== undefined && ttl < 0) { + return undefined + } - return ttl; + return ttl } -function getPublishExchangeProps( - msgOpts: Pick, +export function getPublishExchangeProps( + msgOpts: Pick, ): { - publishingExchangeName: string; - publishingExchangeRoutingKey?: string; + doAssertExchange: boolean + publishingExchangeName: string + publishingExchangeRoutingKey?: string } { - const { scope, exchangeName } = msgOpts; - - if (scope === interfaces.Scope.USER) { - return { - publishingExchangeName: USER.EXCHANGE, - publishingExchangeRoutingKey: `${exchangeName}.${USER.EXCHANGE}`, - }; - } - if (scope === interfaces.Scope.SESSION) { - return { - publishingExchangeName: SESSION.EXCHANGE, - publishingExchangeRoutingKey: `${exchangeName}.${SESSION.EXCHANGE}`, - }; - } - if (scope === interfaces.Scope.BROADCAST) { - return { - publishingExchangeName: BROADCAST.EXCHANGE, - publishingExchangeRoutingKey: `${exchangeName}.${BROADCAST.EXCHANGE}`, - }; - } - if (scope === interfaces.Scope.FRONTEND) { - // publishing to this exchange is not permitted, but let's have everything normalized. - return { - publishingExchangeName: FRONTEND.EXCHANGE, - }; - } - - return { publishingExchangeName: exchangeName }; + const { scope, exchangeName } = msgOpts + + if (scope === interfaces.Scope.USER) { + return { + publishingExchangeName: USER.EXCHANGE, + publishingExchangeRoutingKey: `${exchangeName}.${USER.EXCHANGE}`, + doAssertExchange: flags.ASSERT_NON_INTERNAL_EXCHANGES, + } + } + if (scope === interfaces.Scope.SESSION) { + return { + publishingExchangeName: SESSION.EXCHANGE, + publishingExchangeRoutingKey: `${exchangeName}.${SESSION.EXCHANGE}`, + doAssertExchange: flags.ASSERT_NON_INTERNAL_EXCHANGES, + } + } + if (scope === interfaces.Scope.BROADCAST) { + return { + publishingExchangeName: BROADCAST.EXCHANGE, + publishingExchangeRoutingKey: `${exchangeName}.${BROADCAST.EXCHANGE}`, + doAssertExchange: flags.ASSERT_NON_INTERNAL_EXCHANGES, + } + } + if (scope === interfaces.Scope.FRONTEND) { + // publishing to this exchange is not permitted, but let's have everything normalized. + return { + publishingExchangeName: FRONTEND.EXCHANGE, + doAssertExchange: flags.ASSERT_NON_INTERNAL_EXCHANGES, + } + } + + return { publishingExchangeName: exchangeName, doAssertExchange: true } } diff --git a/src/lib/message.ts b/src/lib/message.ts index 2c2c741..8f5534b 100644 --- a/src/lib/message.ts +++ b/src/lib/message.ts @@ -1,3 +1,3 @@ -export * from "./message.decorator"; -export * from "./message.decorator_utils"; -export * from "./message.interfaces"; +export * from './message.decorator' +export * from './message.decorator_utils' +export * from './message.interfaces' diff --git a/src/lib/message.validation.ts b/src/lib/message.validation.ts index 819d526..8314040 100644 --- a/src/lib/message.validation.ts +++ b/src/lib/message.validation.ts @@ -1,46 +1,46 @@ -import _ from "lodash"; -import * as helper from "./helper"; -import * as interfaces from "./message.interfaces"; -import { ValidationError } from "./validation.interfaces"; -import * as validationIris from "./validation.iris"; +import _ from 'lodash' +import * as helper from './helper' +import * as interfaces from './message.interfaces' +import { ValidationError } from './validation.interfaces' +import * as validationIris from './validation.iris' export function validateProcessedMessageConfig( - msgConfig: interfaces.MessageI, - target: Object, + msgConfig: interfaces.MessageI, + target: Object, ): void { - const { name, routingKey, exchangeType, deadLetter } = msgConfig; - const targetClassName = helper.getTargetConstructor(target).name; + const { name, routingKey, exchangeType, deadLetter } = msgConfig + const targetClassName = helper.getTargetConstructor(target).name - if (_.isEmpty(name)) { - throw new ValidationError( - "MESSAGE_REQUIRES_NAME", - `@Message(${targetClassName}) requires a non empty name to be set`, - { - targetClassName: helper.getTargetConstructor(target).name, - }, - ); - } + if (_.isEmpty(name)) { + throw new ValidationError( + 'MESSAGE_REQUIRES_NAME', + `@Message(${targetClassName}) requires a non empty name to be set`, + { + targetClassName: helper.getTargetConstructor(target).name, + }, + ) + } - validationIris.throwIfValueIsInvalidCaseFormat( - name, - "MESSAGE_INVALID_NAME", - `Name for @Message(${helper.getTargetConstructor(target).name})`, - ); + validationIris.throwIfValueIsInvalidCaseFormat( + name, + 'MESSAGE_INVALID_NAME', + `Name for @Message(${helper.getTargetConstructor(target).name})`, + ) - if (routingKey !== undefined) { - validationIris.throwIfValueIsInvalidCaseFormat( - routingKey, - "MESSAGE_INVALID_ROUTING_KEY", - `Routing Key for @Message(${targetClassName})`, - exchangeType === interfaces.ExchangeType.topic, - ); - } + if (routingKey !== undefined) { + validationIris.throwIfValueIsInvalidCaseFormat( + routingKey, + 'MESSAGE_INVALID_ROUTING_KEY', + `Routing Key for @Message(${targetClassName})`, + exchangeType === interfaces.ExchangeType.topic, + ) + } - if (deadLetter !== undefined) { - validationIris.throwIfValueIsInvalidCaseFormat( - deadLetter, - "MESSAGE_INVALID_DEAD_LETTER_NAME", - `DeadLetter for @Message(${targetClassName})`, - ); - } + if (deadLetter !== undefined) { + validationIris.throwIfValueIsInvalidCaseFormat( + deadLetter, + 'MESSAGE_INVALID_DEAD_LETTER_NAME', + `DeadLetter for @Message(${targetClassName})`, + ) + } } diff --git a/src/lib/message_handler.decorator.ts b/src/lib/message_handler.decorator.ts index a3ffec1..df3b090 100644 --- a/src/lib/message_handler.decorator.ts +++ b/src/lib/message_handler.decorator.ts @@ -1,114 +1,105 @@ -import * as uuid from "uuid"; -import * as helper from "./helper"; -import * as message from "./message"; -import * as decoratorUtils from "./message_handler.decorator_utils"; -import * as interfaces from "./message_handler.interfaces"; -import * as storage from "./storage"; -import * as validationI from "./validation.interfaces"; +import { randomUUID } from 'node:crypto' +import * as helper from './helper' +import * as message from './message' +import * as decoratorUtils from './message_handler.decorator_utils' +import type * as interfaces from './message_handler.interfaces' +import * as storage from './storage' +import * as validationI from './validation.interfaces' /** * AMQP queue decorator. */ export const MessageHandler = - ( - config: interfaces.MessageHandlerI = {}, - replyMessageClass?: Object, - ): MethodDecorator => - ( - target: Object, - propertyKey: string | symbol, - descriptor: PropertyDescriptor, - ): void => { - const targetName = helper.getTargetConstructor(target).name; - const isStaticMethod = target instanceof Function; - const targetConstructor = isStaticMethod ? target : target.constructor; - const targetMessage = manageAutoDecoratedArguments(target, propertyKey); + ( + config: interfaces.MessageHandlerI = {}, + replyMessageClass?: Object, + ): MethodDecorator => + ( + target: Object, + propertyKey: string | symbol, + descriptor: PropertyDescriptor, + ): void => { + const targetName = helper.getTargetConstructor(target).name + const isStaticMethod = target instanceof Function + const targetConstructor = isStaticMethod ? target : target.constructor + const targetMessage = manageAutoDecoratedArguments(target, propertyKey) - if ( - replyMessageClass !== undefined && - !message.isMessageDecoratedClass(replyMessageClass) - ) { - throw new validationI.ValidationError( - "INVALID_HANDLER_REPLY_CLASS", - "MessageHandler() replyClass should be class decorated with @Message()", - { - target: helper.getTargetConstructor(target).name, - method: propertyKey, - }, - ); - } + if ( + replyMessageClass !== undefined && + !message.isMessageDecoratedClass(replyMessageClass) + ) { + throw new validationI.ValidationError( + 'INVALID_HANDLER_REPLY_CLASS', + 'MessageHandler() replyClass should be class decorated with @Message()', + { + target: helper.getTargetConstructor(target).name, + method: propertyKey, + }, + ) + } - let handler: interfaces.MessageHandlerMetadataI = { - kind: "NO_REPLY", - uuid: uuid.v4(), - target, - targetClassName: targetName, - methodName: propertyKey, - isStaticMethod, - descriptor, - messageClass: targetMessage, - origDecoratorConfig: config, - callback: <(...args: unknown[]) => Promise>descriptor.value, - }; + let handler: interfaces.MessageHandlerMetadataI = { + kind: 'NO_REPLY', + uuid: randomUUID(), + target, + targetClassName: targetName, + methodName: propertyKey, + isStaticMethod, + descriptor, + messageClass: targetMessage, + origDecoratorConfig: config, + callback: <(...args: unknown[]) => Promise>descriptor.value, + } - if (replyMessageClass !== undefined) { - handler = { - ...handler, - kind: "WITH_REPLY", - replyMessageClass, - replyMessageClassName: - helper.getTargetConstructor(replyMessageClass).name, - }; - } + if (replyMessageClass !== undefined) { + handler = { + ...handler, + kind: 'WITH_REPLY', + replyMessageClass, + replyMessageClassName: + helper.getTargetConstructor(replyMessageClass).name, + } + } - // a class can have multiple @MessageHandler() methods - const handlers = [ - ...decoratorUtils.getMessageHandlerDecoratedMethods(targetConstructor), - handler, - ]; + // a class can have multiple @MessageHandler() methods + const handlers = [ + ...decoratorUtils.getMessageHandlerDecoratedMethods(targetConstructor), + handler, + ] - storage.SetMetadata( - storage.IRIS_MESSAGE_HANDLERS_META, - handlers, - )(targetConstructor); + storage.SetMetadata( + storage.IRIS_MESSAGE_HANDLERS_META, + handlers, + )(targetConstructor) - const msgHandlers = decoratorUtils.hasHandlers(targetMessage) - ? decoratorUtils.getHandlers(targetMessage) - : []; - msgHandlers.push(targetConstructor); - storage.SetMetadata( - storage.IRIS_MESSAGE_HANDLERS, - msgHandlers, - )(targetConstructor); - }; + decoratorUtils.addHandlerForMsg(targetMessage, targetConstructor) + } function manageAutoDecoratedArguments( - target: Object, - propertyKey: string | symbol, -): Object { - const methodArgs = <(typeof Function)[]>( - Reflect.getMetadata("design:paramtypes", target, propertyKey) - ); - const targetMessage: Object[] = methodArgs.filter( - message.isMessageDecoratedClass, - ); + target: Object, + propertyKey: string | symbol, +): Function { + const methodArgs = <(typeof Function)[]>( + Reflect.getMetadata('design:paramtypes', target, propertyKey) + ) + const targetMessage = methodArgs.filter(message.isMessageDecoratedClass) - if (targetMessage.length !== 1) { - throwTargetMsgError(target, propertyKey); - } + if (targetMessage.length !== 1) { + throwTargetMsgError(target, propertyKey) + } - return targetMessage[0]; + return targetMessage[0] } function throwTargetMsgError( - target: Object, - propertyKey: string | symbol, + target: Object, + propertyKey: string | symbol, ): never { - throw new validationI.ValidationError( - "INVALID_HANDLER_CONFIG", - "MessageHandler() should be a method accepting exactly one argument of a class decorated with @Message()", - { - target: helper.getTargetConstructor(target).name, - method: propertyKey, - }, - ); + throw new validationI.ValidationError( + 'INVALID_HANDLER_CONFIG', + 'MessageHandler() should be a method accepting exactly one argument of a class decorated with @Message()', + { + target: helper.getTargetConstructor(target).name, + method: propertyKey, + }, + ) } diff --git a/src/lib/message_handler.decorator_utils.ts b/src/lib/message_handler.decorator_utils.ts index 30d8270..f4e3fe9 100644 --- a/src/lib/message_handler.decorator_utils.ts +++ b/src/lib/message_handler.decorator_utils.ts @@ -1,60 +1,60 @@ -import * as helper from "./helper"; -import * as message from "./message"; -import * as interfaces from "./message_handler.interfaces"; -import * as process from "./message_handler.process"; -import * as storage from "./storage"; -import * as validationI from "./validation.interfaces"; +import * as helper from './helper' +import * as message from './message' +import type * as interfaces from './message_handler.interfaces' +import * as process from './message_handler.process' +import * as storage from './storage' +import * as validationI from './validation.interfaces' type queueHandlerMapT = Record< - string, - interfaces.ProcessedMessageHandlerMetadataI | undefined ->; + string, + interfaces.ProcessedMessageHandlerMetadataI | undefined +> /** * Whether passed handlerClass has any @MessageHandler() deocrated methods */ export function hasMessageHandlerDecoratedMethods( - handlerClass: Object, + handlerClass: Object, ): boolean { - return getMessageHandlerDecoratedMethods(handlerClass).length > 0; + return getMessageHandlerDecoratedMethods(handlerClass).length > 0 } /** * Get all processd methods decorated with @MessageHandler() for passed handlerClass */ export function getMessageHandlerDecoratedMethods( - handlerClass: Object, + handlerClass: Object, ): interfaces.MessageHandlerMetadataI[] { - return ( - (Reflect.getMetadata(storage.IRIS_MESSAGE_HANDLERS_META, handlerClass) ?? - []) - ); + return ( + (Reflect.getMetadata(storage.IRIS_MESSAGE_HANDLERS_META, handlerClass) ?? + []) + ) } export function getProcessedMessageHandlerDecoratedMethods( - handlerClass: Object, + handlerClass: Object, ): interfaces.ProcessedMessageHandlerMetadataI[] { - const result = getMessageHandlerDecoratedMethods(handlerClass).map((mh) => ({ - ...mh, - processedConfig: process.processAndValidateConfig( - mh, - message.getProcessedMessageDecoratedClass(mh.messageClass), - ), - })); + const result = getMessageHandlerDecoratedMethods(handlerClass).map((mh) => ({ + ...mh, + processedConfig: process.processAndValidateConfig( + mh, + message.getProcessedMessageDecoratedClass(mh.messageClass), + ), + })) - validateAllHandlersUseDistinctQueues(result); + validateAllHandlersUseDistinctQueues(result) - return result; + return result } /** * Whether @Message() decorated class has a handler or not */ export function hasHandlers(messageClass: Object): boolean { - return ( - Reflect.getMetadata(storage.IRIS_MESSAGE_HANDLERS, messageClass) !== - undefined - ); + return ( + Reflect.getMetadata(storage.IRIS_MESSAGE_HANDLERS, messageClass) !== + undefined + ) } /** @@ -62,45 +62,61 @@ export function hasHandlers(messageClass: Object): boolean { * is handling passed messageClass */ export function getHandlers(messageClass: Object): Object[] { - const handlerClasses = ( - Reflect.getMetadata(storage.IRIS_MESSAGE_HANDLERS, messageClass) - ); - if (handlerClasses === undefined) { - throw new validationI.ValidationError( - "NO_SUCH_MESSAGE_HANDLER", - "This @Message() class does not have any handler", - { - targetClassName: helper.getTargetConstructor(messageClass).name, - }, - ); - } + const handlerClasses = ( + Reflect.getMetadata(storage.IRIS_MESSAGE_HANDLERS, messageClass) + ) + if (handlerClasses === undefined) { + throw new validationI.ValidationError( + 'NO_SUCH_MESSAGE_HANDLER', + 'This @Message() class does not have any handler', + { + targetClassName: helper.getTargetConstructor(messageClass).name, + }, + ) + } - return handlerClasses; + return handlerClasses +} + +/** + * @internal + * Add a handler to the list of handlers for specified messageClass + * Internal, to be used via {@link hasHandlers} and {@link getHandlers} + */ +export function addHandlerForMsg(messageClass: Function, handlerClass: Object) { + const msgHandlers = hasHandlers(messageClass) ? getHandlers(messageClass) : [] + + msgHandlers.push(handlerClass) + + storage.SetMetadata( + storage.IRIS_MESSAGE_HANDLERS, + msgHandlers, + )(messageClass) } function validateAllHandlersUseDistinctQueues( - handlers: interfaces.ProcessedMessageHandlerMetadataI[], + handlers: interfaces.ProcessedMessageHandlerMetadataI[], ): void { - handlers.reduce( - (queueHandlerMap: queueHandlerMapT, handler): queueHandlerMapT => { - const { queueName } = handler.processedConfig; - const otherHandler = queueHandlerMap[queueName]; - if (otherHandler !== undefined) { - const messageClassName = helper.getTargetConstructor( - handler.messageClass, - ).name; - const thisHandlerInfo = `@MessageHandler(${handler.targetClassName}).${handler.methodName}()`; - const otherHandlerInfo = `@MessageHandler(${otherHandler.targetClassName}).${otherHandler.methodName}()`; - throw new validationI.ValidationError( - "DUPLICATE_MESSAGE_HANDLER", - `These two handlers would be exchangeably receiving @Message(${messageClassName}):\n - ${thisHandlerInfo} \n - ${otherHandlerInfo}.\nUsing handler for same @Message class is only supported when handling different routing/binding keys.`, - ); - } + handlers.reduce( + (queueHandlerMap: queueHandlerMapT, handler): queueHandlerMapT => { + const { queueName } = handler.processedConfig + const otherHandler = queueHandlerMap[queueName] + if (otherHandler !== undefined) { + const messageClassName = helper.getTargetConstructor( + handler.messageClass, + ).name + const thisHandlerInfo = `@MessageHandler(${handler.targetClassName}).${handler.methodName}()` + const otherHandlerInfo = `@MessageHandler(${otherHandler.targetClassName}).${otherHandler.methodName}()` + throw new validationI.ValidationError( + 'DUPLICATE_MESSAGE_HANDLER', + `These two handlers would be exchangeably receiving @Message(${messageClassName}):\n - ${thisHandlerInfo} \n - ${otherHandlerInfo}.\nUsing handler for same @Message class is only supported when handling different routing/binding keys.`, + ) + } - queueHandlerMap[queueName] = handler; + queueHandlerMap[queueName] = handler - return queueHandlerMap; - }, - {}, - ); + return queueHandlerMap + }, + {}, + ) } diff --git a/src/lib/message_handler.interfaces.ts b/src/lib/message_handler.interfaces.ts index 19e421f..7410530 100644 --- a/src/lib/message_handler.interfaces.ts +++ b/src/lib/message_handler.interfaces.ts @@ -1,96 +1,96 @@ -import * as amqplib from "amqplib"; +import type * as amqplib from 'amqplib' export enum MessageDeliveryMode { - /** - * Message will be handled by single service instance - */ - PER_SERVICE_INSTANCE = "PER_SERVICE_INSTANCE", - /** - * Message will be handled by all running service instances - */ - PER_SERVICE = "PER_SERVICE", + /** + * Message will be handled by single service instance + */ + PER_SERVICE_INSTANCE = 'PER_SERVICE_INSTANCE', + /** + * Message will be handled by all running service instances + */ + PER_SERVICE = 'PER_SERVICE', } export type queueOptionsT = amqplib.Options.AssertQueue & { - durable: boolean; - autoDelete: boolean; - exclusive: boolean; -}; + durable: boolean + autoDelete: boolean + exclusive: boolean +} -export type handlerCallbackI = (...args: unknown[]) => Promise; +export type handlerCallbackI = (...args: unknown[]) => Promise export interface MessageHandlerI { - bindingKeys?: string[] | string; - /** - * If true, the queue will survive broker restarts (defaults to true) - */ - durable?: boolean; - /** - * If true, the queue will be deleted when the number of consumers drops to zero - * (defaults to false) - */ - autoDelete?: boolean; - /** - * Amount of messages that can be received on queue at same time. - * Set it to some low number (like 1) for events causing a long/resource heavy - * tasks. - */ - prefetch?: number; - messageDeliveryMode?: MessageDeliveryMode; + bindingKeys?: string[] | string + /** + * If true, the queue will survive broker restarts (defaults to true) + */ + durable?: boolean + /** + * If true, the queue will be deleted when the number of consumers drops to zero + * (defaults to false) + */ + autoDelete?: boolean + /** + * Amount of messages that can be received on queue at same time. + * Set it to some low number (like 1) for events causing a long/resource heavy + * tasks. + */ + prefetch?: number + messageDeliveryMode?: MessageDeliveryMode } interface MessageHandlerMetadataBaseI { - kind: "NO_REPLY" | "WITH_REPLY"; - uuid: string; - target: Object; - targetClassName: string; - methodName: string; - isStaticMethod: boolean; - descriptor: PropertyDescriptor; - messageClass: Object; - callback: handlerCallbackI; - origDecoratorConfig: MessageHandlerI; + kind: 'NO_REPLY' | 'WITH_REPLY' + uuid: string + target: Object + targetClassName: string + methodName: string + isStaticMethod: boolean + descriptor: PropertyDescriptor + messageClass: Object + callback: handlerCallbackI + origDecoratorConfig: MessageHandlerI } export interface MessageHandlerMetadataNoReplyI - extends MessageHandlerMetadataBaseI { - kind: "NO_REPLY"; + extends MessageHandlerMetadataBaseI { + kind: 'NO_REPLY' } export interface MessageHandlerMetadataWithReplyI - extends MessageHandlerMetadataBaseI { - kind: "WITH_REPLY"; - replyMessageClass: Object; - replyMessageClassName: string; + extends MessageHandlerMetadataBaseI { + kind: 'WITH_REPLY' + replyMessageClass: Object + replyMessageClassName: string } export type MessageHandlerMetadataI = - | MessageHandlerMetadataWithReplyI - | MessageHandlerMetadataNoReplyI; + | MessageHandlerMetadataWithReplyI + | MessageHandlerMetadataNoReplyI export type ProcessedMessageHandlerMetadataI = MessageHandlerMetadataI & { - processedConfig: ProcessedMessageHandlerConfigI; -}; + processedConfig: ProcessedMessageHandlerConfigI +} export interface ProcessedMessageHandlerConfigI - extends Omit { - bindingKeys: string[]; - messageDeliveryMode: MessageDeliveryMode; - queueOptions: queueOptionsT; - queueName: string; - /** - * Exchange queue needs to bind to, can differ from the one - * found in related ProcessedMessageHandlerMetadataI in some - * cases (like when FRONTEND scope is used). - */ - exchange: string; + extends Omit { + bindingKeys: string[] + messageDeliveryMode: MessageDeliveryMode + queueOptions: queueOptionsT + queueName: string + /** + * Exchange queue needs to bind to, can differ from the one + * found in related ProcessedMessageHandlerMetadataI in some + * cases (like when FRONTEND scope is used). + */ + exchange: string } export interface MessageHandlerParamI { - parameterIndex: number; - handle(msg: unknown): T; + parameterIndex: number + handle(msg: unknown): T } export interface MessageHandlerOptions { - targetMessage: Object; - methodParams: MessageHandlerParamI[]; + targetMessage: Object + methodParams: MessageHandlerParamI[] } diff --git a/src/lib/message_handler.param.decorator.ts b/src/lib/message_handler.param.decorator.ts index 905e9b4..08df691 100644 --- a/src/lib/message_handler.param.decorator.ts +++ b/src/lib/message_handler.param.decorator.ts @@ -1,14 +1,36 @@ -import * as amqplib from "amqplib"; -import { AMQP_MESSAGE_CLASS, SetMetadata } from "./storage"; +import type * as amqplib from 'amqplib' +import _ from 'lodash' +import { AMQP_MESSAGE_CLASS, SetMetadata } from './storage' + +const AMQP_MESSAGE_PROPERTIES = ['content', 'fields', 'properties'] export class AmqpMessage implements amqplib.ConsumeMessage { - content!: Buffer; - fields!: amqplib.ConsumeMessageFields; - properties!: amqplib.MessageProperties; + content!: Buffer + fields!: amqplib.ConsumeMessageFields + properties!: amqplib.MessageProperties } -SetMetadata(AMQP_MESSAGE_CLASS, true)(AmqpMessage); +SetMetadata(AMQP_MESSAGE_CLASS, true)(AmqpMessage) + +export const isAmqpMessageClass = (target: unknown): boolean => { + if (typeof target === 'object' && target !== null) { + if (target instanceof AmqpMessage) { + return true + } + + const objKeys = Object.keys(target) -export const isAmqpMessageClass = (target: Object): boolean => - target === AmqpMessage || - Reflect.getMetadata(AMQP_MESSAGE_CLASS, target) === true; + return ( + objKeys.length === AMQP_MESSAGE_PROPERTIES.length && + _.xor(objKeys, AMQP_MESSAGE_PROPERTIES).length === 0 && + (<{ content: any }>target).content instanceof Buffer + ) + } + if (typeof target === 'function') { + return ( + target === AmqpMessage || Reflect.getMetadata(AMQP_MESSAGE_CLASS, target) + ) + } + + return false +} diff --git a/src/lib/message_handler.process.ts b/src/lib/message_handler.process.ts index 63fffc0..4deb76b 100644 --- a/src/lib/message_handler.process.ts +++ b/src/lib/message_handler.process.ts @@ -1,175 +1,167 @@ -import * as amqplib from "amqplib"; -import _ from "lodash"; -import { - MANAGED_EXCHANGES, - getQueueDefaultsForExchangeName, -} from "./constants"; -import * as helper from "./helper"; -import * as message from "./message"; -import * as interfaces from "./message_handler.interfaces"; -import * as validation from "./message_handler.validation"; -import * as validationI from "./validation.interfaces"; - -const { FRONTEND, DEAD_LETTER } = MANAGED_EXCHANGES; +import type * as amqplib from 'amqplib' +import _ from 'lodash' +import { MANAGED_EXCHANGES, getQueueDefaultsForExchangeName } from './constants' +import * as helper from './helper' +import * as message from './message' +import * as interfaces from './message_handler.interfaces' +import * as validation from './message_handler.validation' +import * as validationI from './validation.interfaces' + +const { FRONTEND, DEAD_LETTER } = MANAGED_EXCHANGES export function processAndValidateConfig( - configIntermediate: interfaces.MessageHandlerMetadataI, - msgMeta: message.ProcessedMessageMetadataI, + configIntermediate: interfaces.MessageHandlerMetadataI, + msgMeta: message.ProcessedMessageMetadataI, ): interfaces.ProcessedMessageHandlerConfigI { - const { exchangeName } = msgMeta.processedConfig; - - const origConfig = configIntermediate.origDecoratorConfig; - - const messageDeliveryMode = - origConfig.messageDeliveryMode ?? - interfaces.MessageDeliveryMode.PER_SERVICE; - let { durable, autoDelete } = getQueueDefaultsForExchangeName( - exchangeName, - origConfig, - ); - - if ( - messageDeliveryMode === interfaces.MessageDeliveryMode.PER_SERVICE_INSTANCE - ) { - durable = false; - autoDelete = true; - } - - if (message.isFrontend(msgMeta)) { - durable = false; - } - - const processedConfig: Omit< - interfaces.ProcessedMessageHandlerConfigI, - "queueName" | "queueOptions" - > = { - ..._.omit(origConfig, ["durable", "autoDelete"]), - messageDeliveryMode, - exchange: message.isFrontend(msgMeta) ? FRONTEND.EXCHANGE : exchangeName, - bindingKeys: getBindingKeys( - configIntermediate, - origConfig.bindingKeys, - msgMeta, - ), - }; - - const queueName = getQueueName( - configIntermediate.uuid, - processedConfig, - msgMeta, - ); - const queueOptions: interfaces.queueOptionsT = { - durable, - autoDelete, - exclusive: false, - ...getQueueExtraOptions(queueName, msgMeta), - }; - - const processedHandlerConfig = { - ...processedConfig, - queueName, - queueOptions, - }; - - validation.validateProcessedHandlerConfig( - processedHandlerConfig, - configIntermediate, - msgMeta, - ); - - return processedHandlerConfig; + const { exchangeName } = msgMeta.processedConfig + + const origConfig = configIntermediate.origDecoratorConfig + + const messageDeliveryMode = + origConfig.messageDeliveryMode ?? interfaces.MessageDeliveryMode.PER_SERVICE + let { durable, autoDelete } = getQueueDefaultsForExchangeName( + exchangeName, + origConfig, + ) + + if ( + messageDeliveryMode === interfaces.MessageDeliveryMode.PER_SERVICE_INSTANCE + ) { + durable = false + autoDelete = true + } + + if (message.isFrontend(msgMeta)) { + durable = false + } + + const processedConfig: Omit< + interfaces.ProcessedMessageHandlerConfigI, + 'queueName' | 'queueOptions' + > = { + ..._.omit(origConfig, ['durable', 'autoDelete']), + messageDeliveryMode, + exchange: message.isFrontend(msgMeta) ? FRONTEND.EXCHANGE : exchangeName, + bindingKeys: getBindingKeys( + configIntermediate, + origConfig.bindingKeys, + msgMeta, + ), + } + + const queueName = getQueueName( + configIntermediate.uuid, + processedConfig, + msgMeta, + ) + const queueOptions: interfaces.queueOptionsT = { + durable, + autoDelete, + exclusive: false, + ...getQueueExtraOptions(queueName, msgMeta), + } + + const processedHandlerConfig = { + ...processedConfig, + queueName, + queueOptions, + } + + validation.validateProcessedHandlerConfig( + processedHandlerConfig, + configIntermediate, + msgMeta, + ) + + return processedHandlerConfig } function getBindingKeys( - handlerMetadata: interfaces.MessageHandlerMetadataI, - bindingKeys: string[] | string | undefined, - msgMeta: message.ProcessedMessageMetadataI, + handlerMetadata: interfaces.MessageHandlerMetadataI, + bindingKeys: string[] | string | undefined, + msgMeta: message.ProcessedMessageMetadataI, ): string[] { - const { exchangeName, exchangeType } = msgMeta.processedConfig; - - validation.validateBindingKeysForConfig( - handlerMetadata, - bindingKeys, - msgMeta, - ); - - if (message.isFrontend(msgMeta)) { - // this decision assumes that FRONTEND exchange is not `direct` - return [`#.${exchangeName}`]; - } - - if (exchangeType === message.ExchangeType.fanout) { - return [`#.${exchangeName}`]; - } - - let bKeys = [bindingKeys].flat().filter((s) => !_.isEmpty(s)); - validation.validateBindingKeys(bKeys, handlerMetadata, msgMeta); - - // msgMeta.processedConfig.routingKey contains either a routingKey or - // exchange name if rk is empty. - if (bKeys.length === 0 && !_.isEmpty(msgMeta.processedConfig.routingKey)) { - bKeys = [msgMeta.processedConfig.routingKey]; - } - - if (exchangeType === message.ExchangeType.direct && bKeys.length !== 1) { - throw new validationI.ValidationError( - "MESSAGE_HANDLER_INVALID_BINDING_KEYS", - `MessageHandler() ${exchangeType} Message (${exchangeName}) requires exactly one binding key.`, - { - bindingKeys, - }, - ); - } - - return bKeys; + const { exchangeName, exchangeType } = msgMeta.processedConfig + + validation.validateBindingKeysForConfig(handlerMetadata, bindingKeys, msgMeta) + + if (message.isFrontend(msgMeta)) { + // this decision assumes that FRONTEND exchange is not `direct` + return [`#.${exchangeName}`] + } + + if (exchangeType === message.ExchangeType.fanout) { + return [`#.${exchangeName}`] + } + + let bKeys = [bindingKeys].flat().filter((s) => !_.isEmpty(s)) + validation.validateBindingKeys(bKeys, handlerMetadata, msgMeta) + + // msgMeta.processedConfig.routingKey contains either a routingKey or + // exchange name if rk is empty. + if (bKeys.length === 0 && !_.isEmpty(msgMeta.processedConfig.routingKey)) { + bKeys = [msgMeta.processedConfig.routingKey] + } + + if (exchangeType === message.ExchangeType.direct && bKeys.length !== 1) { + throw new validationI.ValidationError( + 'MESSAGE_HANDLER_INVALID_BINDING_KEYS', + `MessageHandler() ${exchangeType} Message (${exchangeName}) requires exactly one binding key.`, + { + bindingKeys, + }, + ) + } + + return bKeys } function getQueueName( - handlerUuid: string, - config: Pick< - interfaces.ProcessedMessageHandlerConfigI, - "messageDeliveryMode" | "bindingKeys" - >, - msgMeta: message.ProcessedMessageMetadataI, + handlerUuid: string, + config: Pick< + interfaces.ProcessedMessageHandlerConfigI, + 'messageDeliveryMode' | 'bindingKeys' + >, + msgMeta: message.ProcessedMessageMetadataI, ): string { - const { messageDeliveryMode, bindingKeys } = config; - const { exchangeName, exchangeType } = msgMeta.processedConfig; - const qName: string[] = [helper.getServiceName(), exchangeName]; - - const appendBindingKeys = - (exchangeType === message.ExchangeType.direct || - exchangeType === message.ExchangeType.topic) && - !message.isFrontend(msgMeta); - - if (appendBindingKeys) { - qName.push(bindingKeys.join("-")); - } - - if ( - messageDeliveryMode === interfaces.MessageDeliveryMode.PER_SERVICE_INSTANCE - ) { - qName.push(helper.getHostName(), handlerUuid); - } - - return qName.filter((part) => part.length > 0).join("."); + const { messageDeliveryMode, bindingKeys } = config + const { exchangeName, exchangeType } = msgMeta.processedConfig + const qName: string[] = [helper.getServiceName(), exchangeName] + + const appendBindingKeys = + (exchangeType === message.ExchangeType.direct || + exchangeType === message.ExchangeType.topic) && + !message.isFrontend(msgMeta) + + if (appendBindingKeys) { + qName.push(bindingKeys.join('-')) + } + + if ( + messageDeliveryMode === interfaces.MessageDeliveryMode.PER_SERVICE_INSTANCE + ) { + qName.push(helper.getHostName(), handlerUuid) + } + + return qName.filter((part) => part.length > 0).join('.') } function getQueueExtraOptions( - queueName: string, - msgMeta: message.ProcessedMessageMetadataI, + queueName: string, + msgMeta: message.ProcessedMessageMetadataI, ): Partial { - const opts: amqplib.Options.AssertQueue = {}; + const opts: amqplib.Options.AssertQueue = {} - const { ttl, deadLetter } = msgMeta.processedConfig; + const { ttl, deadLetter } = msgMeta.processedConfig - if (ttl !== undefined) { - opts.messageTtl = ttl; - } + if (ttl !== undefined) { + opts.messageTtl = ttl + } - if (!_.isEmpty(deadLetter)) { - opts.deadLetterExchange = deadLetter; - opts.deadLetterRoutingKey = `${DEAD_LETTER.PREFIX}${queueName}`; - } + if (!_.isEmpty(deadLetter)) { + opts.deadLetterExchange = deadLetter + opts.deadLetterRoutingKey = `${DEAD_LETTER.PREFIX}${queueName}` + } - return opts; + return opts } diff --git a/src/lib/message_handler.ts b/src/lib/message_handler.ts index 50bfd94..7b99e56 100644 --- a/src/lib/message_handler.ts +++ b/src/lib/message_handler.ts @@ -1,4 +1,5 @@ -export * from "./message_handler.interfaces"; -export * from "./message_handler.decorator"; -export * from "./message_handler.decorator_utils"; -export { AmqpMessage } from "./message_handler.param.decorator"; +export * from './message_handler.interfaces' +export * from './message_handler.decorator' +export * from './message_handler.decorator_utils' +export { AmqpMessage } from './message_handler.param.decorator' +export { MDC } from './mdc' diff --git a/src/lib/message_handler.validation.ts b/src/lib/message_handler.validation.ts index 8a4991b..be2cc90 100644 --- a/src/lib/message_handler.validation.ts +++ b/src/lib/message_handler.validation.ts @@ -1,91 +1,91 @@ -import * as helper from "./helper"; -import * as message from "./message"; -import * as messageI from "./message.interfaces"; -import * as interfaces from "./message_handler.interfaces"; -import { SnapshotRequested } from "./subscription.messages"; -import { ValidationError } from "./validation.interfaces"; -import * as validationIris from "./validation.iris"; +import * as helper from './helper' +import * as message from './message' +import * as messageI from './message.interfaces' +import * as interfaces from './message_handler.interfaces' +import { SnapshotRequested } from './subscription.messages' +import { ValidationError } from './validation.interfaces' +import * as validationIris from './validation.iris' export function validateProcessedHandlerConfig( - processedHandlerConfig: interfaces.ProcessedMessageHandlerConfigI, - handlerMetadata: interfaces.MessageHandlerMetadataI, - msgMeta: messageI.ProcessedMessageMetadataI, + processedHandlerConfig: interfaces.ProcessedMessageHandlerConfigI, + handlerMetadata: interfaces.MessageHandlerMetadataI, + msgMeta: messageI.ProcessedMessageMetadataI, ): void { - if ( - processedHandlerConfig.messageDeliveryMode === - interfaces.MessageDeliveryMode.PER_SERVICE_INSTANCE - ) { - const { scope } = msgMeta.processedConfig; - if (scope === messageI.Scope.FRONTEND) { - throw new ValidationError( - "PER_SERVICE_INSTANCE_NOT_SUPPORTED_FOR_FRONTEND_SCOPE", - "FRONTEND scope can not be used with PER_SERVICE_INSTANCE delivery", - getErrorDetails(handlerMetadata, msgMeta), - ); - } + if ( + processedHandlerConfig.messageDeliveryMode === + interfaces.MessageDeliveryMode.PER_SERVICE_INSTANCE + ) { + const { scope } = msgMeta.processedConfig + if (scope === messageI.Scope.FRONTEND) { + throw new ValidationError( + 'PER_SERVICE_INSTANCE_NOT_SUPPORTED_FOR_FRONTEND_SCOPE', + 'FRONTEND scope can not be used with PER_SERVICE_INSTANCE delivery', + getErrorDetails(handlerMetadata, msgMeta), + ) + } - if ( - helper.classIsSameOrSubclassOf( - handlerMetadata.messageClass, - SnapshotRequested, - ) - ) { - throw new ValidationError( - "PER_SERVICE_INSTANCE_NOT_SUPPORTED_WITH_SNAPSHOT_REQUESTED_MESSAGE", - `@Message(${SnapshotRequested.name}) can not be used with PER_SERVICE_INSTANCE delivery`, - getErrorDetails(handlerMetadata, msgMeta), - ); - } - } + if ( + helper.classIsSameOrSubclassOf( + handlerMetadata.messageClass, + SnapshotRequested, + ) + ) { + throw new ValidationError( + 'PER_SERVICE_INSTANCE_NOT_SUPPORTED_WITH_SNAPSHOT_REQUESTED_MESSAGE', + `@Message(${SnapshotRequested.name}) can not be used with PER_SERVICE_INSTANCE delivery`, + getErrorDetails(handlerMetadata, msgMeta), + ) + } + } } export function validateBindingKeysForConfig( - handlerMetadata: interfaces.MessageHandlerMetadataI, - bindingKeys: string[] | string | undefined, - msgMeta: message.ProcessedMessageMetadataI, + handlerMetadata: interfaces.MessageHandlerMetadataI, + bindingKeys: string[] | string | undefined, + msgMeta: message.ProcessedMessageMetadataI, ): void { - if (bindingKeys !== undefined) { - const { exchangeType } = msgMeta.processedConfig; - if (message.isFrontend(msgMeta)) { - throw new ValidationError( - "MESSAGE_HANDLER_BINDING_KEYS_ARE_OVERRIDDEN_FOR_FRONTEND_SCOPE", - `@Message(${msgMeta.targetClassName}) is FRONTEND, bindingKeys are set internally in this case`, - getErrorDetails(handlerMetadata, msgMeta), - ); - } - if (exchangeType === message.ExchangeType.fanout) { - throw new ValidationError( - "MESSAGE_HANDLER_BINDING_KEYS_HAVE_NO_EFFECT_FOR_FANOUT_EXCHANGE", - `@Message(${msgMeta.targetClassName}) is FANOUT exchangeType, bindingKeys are ignored in this case`, - getErrorDetails(handlerMetadata, msgMeta), - ); - } - } + if (bindingKeys !== undefined) { + const { exchangeType } = msgMeta.processedConfig + if (message.isFrontend(msgMeta)) { + throw new ValidationError( + 'MESSAGE_HANDLER_BINDING_KEYS_ARE_OVERRIDDEN_FOR_FRONTEND_SCOPE', + `@Message(${msgMeta.targetClassName}) is FRONTEND, bindingKeys are set internally in this case`, + getErrorDetails(handlerMetadata, msgMeta), + ) + } + if (exchangeType === message.ExchangeType.fanout) { + throw new ValidationError( + 'MESSAGE_HANDLER_BINDING_KEYS_HAVE_NO_EFFECT_FOR_FANOUT_EXCHANGE', + `@Message(${msgMeta.targetClassName}) is FANOUT exchangeType, bindingKeys are ignored in this case`, + getErrorDetails(handlerMetadata, msgMeta), + ) + } + } } export function validateBindingKeys( - bindingKeys: string[], - handlerMeta: interfaces.MessageHandlerMetadataI, - msgMeta: message.ProcessedMessageMetadataI, + bindingKeys: string[], + handlerMeta: interfaces.MessageHandlerMetadataI, + msgMeta: message.ProcessedMessageMetadataI, ): void { - const { exchangeType } = msgMeta.processedConfig; - for (const bindingKey of bindingKeys) { - validationIris.throwIfValueIsInvalidCaseFormat( - bindingKey, - "MESSAGE_HANDLER_INVALID_BINDING_KEYS", - `@MessageHandler(${handlerMeta.targetClassName}).${handlerMeta.methodName}: bindingKey '${bindingKey}'`, - exchangeType === message.ExchangeType.topic, - ); - } + const { exchangeType } = msgMeta.processedConfig + for (const bindingKey of bindingKeys) { + validationIris.throwIfValueIsInvalidCaseFormat( + bindingKey, + 'MESSAGE_HANDLER_INVALID_BINDING_KEYS', + `@MessageHandler(${handlerMeta.targetClassName}).${handlerMeta.methodName}: bindingKey '${bindingKey}'`, + exchangeType === message.ExchangeType.topic, + ) + } } function getErrorDetails( - handlerMetadata: interfaces.MessageHandlerMetadataI, - msgMeta: message.ProcessedMessageMetadataI, + handlerMetadata: interfaces.MessageHandlerMetadataI, + msgMeta: message.ProcessedMessageMetadataI, ): Record { - return { - handler: handlerMetadata.targetClassName, - method: handlerMetadata.methodName, - message: msgMeta.targetClassName, - }; + return { + handler: handlerMetadata.targetClassName, + method: handlerMetadata.methodName, + message: msgMeta.targetClassName, + } } diff --git a/src/lib/publish.interfaces.ts b/src/lib/publish.interfaces.ts index e6eefb3..12f02eb 100644 --- a/src/lib/publish.interfaces.ts +++ b/src/lib/publish.interfaces.ts @@ -1,12 +1,7 @@ -import * as amqplib from "amqplib"; +import type * as amqplib from 'amqplib' export declare type PublishOptionsI = { - routingKey?: string; - userId?: string; - amqpPublishOpts?: amqplib.Options.Publish; -}; - -export type PublisherI = ( - msg: T, - pubOpts?: PublishOptionsI, -) => Promise; + routingKey?: string + userId?: string + amqpPublishOpts?: amqplib.Options.Publish +} diff --git a/src/lib/publish.ts b/src/lib/publish.ts index 49e33fb..b7b7813 100644 --- a/src/lib/publish.ts +++ b/src/lib/publish.ts @@ -1,39 +1,90 @@ -import * as amqplib from "amqplib"; -import { ClassConstructor } from "class-transformer"; -import _ from "lodash"; -import * as uuid from "uuid"; -import { getLogger } from "../logger"; -import * as amqpHelper from "./amqp.helper"; -import { connection } from "./connection"; -import * as constants from "./constants"; -import flags from "./flags"; -import * as helper from "./helper"; -import * as message from "./message"; -import * as publishI from "./publish.interfaces"; -import * as validation from "./validation"; - -export * from "./publish.interfaces"; - -const logger = getLogger("Iris:Publish"); -const { MESSAGE_HEADERS } = constants; - -export function getPublisher( - messageClass: ClassConstructor, -): publishI.PublisherI { - getMessageMetaFromClass( - messageClass, - "getPublisher() passed argument should be class decorated with @Message()", - ); - - return async (msg: T, pubOpts?: publishI.PublishOptionsI): Promise => - publish(messageClass, msg, pubOpts); +import { randomUUID } from 'node:crypto' +import type * as amqplib from 'amqplib' +import type { ClassConstructor } from 'class-transformer' +import _ from 'lodash' +import logger from '../logger' +import * as amqpHelper from './amqp.helper' +import { connection } from './connection' +import * as constants from './constants' +import { asError } from './errors' +import flags from './flags' +import * as helper from './helper' +import * as message from './message' +import { getPublishExchangeProps } from './message.process' +import { + AmqpMessage, + isAmqpMessageClass, +} from './message_handler.param.decorator' +import type * as publishI from './publish.interfaces' +import * as validation from './validation' + +export * from './publish.interfaces' + +const TAG = 'Iris:Publish' + +const { MESSAGE_HEADERS } = constants + +export function getPublisher(messageClass: ClassConstructor) { + getMessageMetaFromClass( + messageClass, + 'getPublisher() passed argument should be class decorated with @Message()', + ) + + return async (msg: T, pubOpts?: publishI.PublishOptionsI): Promise => + publish(messageClass, msg, pubOpts) +} + +export function getUserPublisher(messageClass: ClassConstructor) { + getMessageMetaFromClass( + messageClass, + 'getPublisher() passed argument should be class decorated with @Message()', + ) + + return async ( + msg: T, + user: string | AmqpMessage, + pubOpts?: publishI.PublishOptionsI, + ): Promise => publishToUser(messageClass, msg, user, pubOpts) } export const publish = async ( - messageClass: ClassConstructor, - msg: T, - pubOpts?: publishI.PublishOptionsI, -): Promise => internalPublish(messageClass, msg, pubOpts); + messageClass: ClassConstructor, + msg: T, + pubOpts?: publishI.PublishOptionsI, +): Promise => internalPublish(messageClass, msg, pubOpts) + +/** + * Send the mesaage to `user` exchange, + * ignoring the exchange set on event being published. + * + * @{param user} - user id or AmqpMessage + * when processing a message from user, AmqpMessage can + * be used to get user id from consumed message. + */ +export const publishToUser = async ( + messageClass: ClassConstructor, + msg: T, + user: string | AmqpMessage, + pubOpts?: publishI.PublishOptionsI, +): Promise => { + const userIdString = typeof user === 'string' + const hasOriginalMsg = !userIdString && isAmqpMessageClass(user) + const userId = userIdString + ? user + : _.get(user, `properties.headers[${MESSAGE_HEADERS.MESSAGE.USER_ID}]`) + + if (!_.isString(userId) || _.isEmpty(userId)) { + throw new Error('ERR_IRIS_PUBLISHER_USER_ID_NOT_RESOLVED') + } + + return internalPublish( + messageClass, + msg, + Object.assign({}, pubOpts, { userId }), + hasOriginalMsg ? user : undefined, + message.Scope.USER, + ) +} /** * Copies headers and properties from original message to message being published. @@ -41,191 +92,206 @@ export const publish = async ( * Functionality is exactly the same as when @MessageHandler returns a @Message. */ export const publishReply = async ( - originalMessage: Pick, - messageClass: ClassConstructor, - msg: T, - pubOpts?: publishI.PublishOptionsI, + originalMessage: Pick, + messageClass: ClassConstructor, + msg: T, + pubOpts?: publishI.PublishOptionsI, ): Promise => - internalPublish(messageClass, msg, pubOpts, originalMessage); + internalPublish(messageClass, msg, pubOpts, originalMessage) async function internalPublish( - messageClass: ClassConstructor, - msg: T, - pubOpts?: publishI.PublishOptionsI, - originalMessage?: Pick, + messageClass: ClassConstructor, + msg: T, + pubOpts?: publishI.PublishOptionsI, + originalMessage?: Pick, + overrideScope?: message.Scope, ): Promise { - const msgMeta = getMessageMetaFromClass( - messageClass, - "internalPublish() passed argument should be calss decorated with @Message()", - ); - const msgString = await msg2String(msg, messageClass, msgMeta); - const { exchangeName } = msgMeta.processedConfig; - const routingKey = ( - [pubOpts?.routingKey, msgMeta.processedConfig.routingKey, ""].find( - (s) => s !== undefined, - ) - ); - - return doPublish( - msgMeta, - msgString, - routingKey, - getAmqpBasicProperties(exchangeName, msgMeta, originalMessage, pubOpts), - ); + const msgMeta = getMessageMetaFromClass( + messageClass, + 'internalPublish() passed argument should be calss decorated with @Message()', + ) + const msgString = await msg2String(msg, messageClass, msgMeta) + const { exchangeName } = msgMeta.processedConfig + const routingKey = ( + [pubOpts?.routingKey, msgMeta.processedConfig.routingKey, ''].find( + (s) => s !== undefined, + ) + ) + + return doPublish( + msgMeta, + msgString, + routingKey, + getAmqpBasicProperties(exchangeName, msgMeta, originalMessage, pubOpts), + overrideScope, + ) } export async function doPublish( - msgMeta: message.ProcessedMessageMetadataI, - msg: string, - routingKeyArg: string, - options?: amqplib.Options.Publish, + msgMeta: message.ProcessedMessageMetadataI, + msg: string, + routingKeyArg: string, + options?: amqplib.Options.Publish, + overrideScope?: message.Scope, ): Promise { - validateBeforePublish(msgMeta, options); - - const { publishingExchangeName, publishingExchangeRoutingKey } = - msgMeta.processedConfig; - const routingKey = publishingExchangeRoutingKey ?? routingKeyArg; - - logger.debug("Publishing message", { - msg, - routingKey, - publishingExchangeName, - options: amqpHelper.safeAmqpObjectForLogging(options), - }); - - const channel = await connection.assureDefaultChannel(); - - return channel.publish( - publishingExchangeName, - routingKey, - Buffer.from(msg), - options, - ); + validateBeforePublish(msgMeta, options, overrideScope) + + const { publishingExchangeName, publishingExchangeRoutingKey } = + overrideScope !== undefined + ? getPublishExchangeProps({ + scope: overrideScope, + exchangeName: msgMeta.processedConfig.exchangeName, + }) + : msgMeta.processedConfig + + const routingKey = publishingExchangeRoutingKey ?? routingKeyArg + + logger.debug(TAG, `Publishing message to "${publishingExchangeName}"`, { + evt: msg, + routingKey, + publishingExchangeName, + options: amqpHelper.safeAmqpObjectForLogging(options), + }) + + const channel = await connection.assureDefaultChannel() + + return channel.publish( + publishingExchangeName, + routingKey, + Buffer.from(msg), + options, + ) } function getMessageMetaFromClass( - messageClass: ClassConstructor, - onErrMsg: string, + messageClass: ClassConstructor, + onErrMsg: string, ): message.ProcessedMessageMetadataI { - try { - return message.getProcessedMessageDecoratedClass(messageClass); - } catch (error) { - logger.error(onErrMsg, error, { messageClass }); - throw new Error("ERR_IRIS_PUBLISHER_INVALID_MESSAGE_CLASS"); - } + try { + return message.getProcessedMessageDecoratedClass(messageClass) + } catch (err) { + logger.error(TAG, onErrMsg, { err: asError(err), messageClass }) + throw new Error('ERR_IRIS_PUBLISHER_INVALID_MESSAGE_CLASS') + } } async function msg2String( - msg: T, - messageClass: ClassConstructor, - msgMeta: message.ProcessedMessageMetadataI, + msg: T, + messageClass: ClassConstructor, + msgMeta: message.ProcessedMessageMetadataI, ): Promise { - await validation.validationClass.convertToTargetClass( - msg, - messageClass, - msgMeta.validation, - flags.DISABLE_MESSAGE_PRODUCE_VALIDATION, - ); - - return JSON.stringify(msg); + await validation.validationClass.convertToTargetClass( + msg, + messageClass, + msgMeta.validation, + flags.DISABLE_MESSAGE_PRODUCE_VALIDATION, + ) + + return JSON.stringify(msg) } function getAmqpBasicProperties( - exchangeName: string, - msgMeta: message.ProcessedMessageMetadataI, - originalMsg?: Pick, - pubOpts?: publishI.PublishOptionsI, + exchangeName: string, + msgMeta: message.ProcessedMessageMetadataI, + originalMsg?: Pick, + pubOpts?: publishI.PublishOptionsI, ): Partial { - const amqpProperties = getAmqpPropsWithoutHeaders(originalMsg, pubOpts); - const amqpHeaders = getAmqpHeaders(exchangeName, originalMsg, pubOpts); - - if (msgMeta.processedConfig.scope !== message.Scope.INTERNAL) { - // never propagate JWT when "leaving" backend - delete amqpHeaders[constants.MESSAGE_HEADERS.MESSAGE.JWT]; - } - - if (pubOpts?.userId !== undefined) { - const serviceId = helper.getServiceName(); - const correlationId = uuid.v4(); - // when overriding user header make sure - // to clean possible existing event context properties - amqpProperties.correlationId = correlationId; - amqpHeaders[MESSAGE_HEADERS.MESSAGE.ORIGIN_SERVICE_ID] = serviceId; - amqpHeaders[MESSAGE_HEADERS.MESSAGE.USER_ID] = pubOpts.userId; - delete amqpHeaders[MESSAGE_HEADERS.MESSAGE.ROUTER]; - delete amqpHeaders[MESSAGE_HEADERS.MESSAGE.SESSION_ID]; - } - - return { - ...amqpProperties, - headers: amqpHeaders, - }; + const amqpProperties = getAmqpPropsWithoutHeaders(originalMsg, pubOpts) + const amqpHeaders = getAmqpHeaders(exchangeName, originalMsg, pubOpts) + + // TODO: subscription updates, set cache ttl, subcription id etc. + + if (msgMeta.processedConfig.scope !== message.Scope.INTERNAL) { + // never propagate JWT when "leaving" backend + delete amqpHeaders[constants.MESSAGE_HEADERS.MESSAGE.JWT] + } + + if (pubOpts?.userId !== undefined) { + const serviceId = helper.getServiceName() + const correlationId = randomUUID() + + // when overriding user header make sure + // to clean possible existing event context properties + amqpProperties.correlationId = correlationId + amqpHeaders[MESSAGE_HEADERS.MESSAGE.ORIGIN_SERVICE_ID] = serviceId + amqpHeaders[MESSAGE_HEADERS.MESSAGE.USER_ID] = pubOpts.userId + delete amqpHeaders[MESSAGE_HEADERS.MESSAGE.ROUTER] + delete amqpHeaders[MESSAGE_HEADERS.MESSAGE.SESSION_ID] + } + + return { + ...amqpProperties, + headers: amqpHeaders, + } } function getAmqpPropsWithoutHeaders( - originalMsg?: Pick, - pubOpts?: publishI.PublishOptionsI, -): Partial> { - const forceOptions = pubOpts?.amqpPublishOpts; - const correlationId = uuid.v4(); - const inheritedProperties: - | Omit - | object = _.chain(originalMsg).get("properties").omit("headers").value(); - const forcedProperties = _.chain(forceOptions).omit("headers").value(); - - return { - correlationId, // will be overridden if found in originalMsg - ...inheritedProperties, - ...forcedProperties, - }; + originalMsg?: Pick, + pubOpts?: publishI.PublishOptionsI, +): Partial> { + const forceOptions = pubOpts?.amqpPublishOpts + const correlationId = randomUUID() + + const inheritedProperties: + | Omit + | object = _.chain(originalMsg).get('properties').omit('headers').value() + + const forcedProperties = _.chain(forceOptions).omit('headers').value() + + return { + correlationId, // will be overridden if found in originalMsg + ...inheritedProperties, + ...forcedProperties, + } } function getAmqpHeaders( - exchangeName: string, - originalMsg?: Pick, - pubOpts?: publishI.PublishOptionsI, + exchangeName: string, + originalMsg?: Pick, + pubOpts?: publishI.PublishOptionsI, ): amqplib.MessagePropertyHeaders { - const forceOptions = pubOpts?.amqpPublishOpts; - const serviceId = helper.getServiceName(); - const inheritedHeaders = - originalMsg !== undefined ? originalMsg.properties.headers : {}; - const forcedHeaders = forceOptions !== undefined ? forceOptions.headers : {}; - - return { - // set ORIGIN_SERVICE_ID but let it be overriden by inheritedHeaders if exists there - [MESSAGE_HEADERS.MESSAGE.ORIGIN_SERVICE_ID]: helper.getServiceName(), - ...inheritedHeaders, - // force override these default headers - [MESSAGE_HEADERS.MESSAGE.CURRENT_SERVICE_ID]: serviceId, - [MESSAGE_HEADERS.MESSAGE.INSTANCE_ID]: helper.getHostName(), - [MESSAGE_HEADERS.MESSAGE.EVENT_TYPE]: exchangeName, - // manually passed options should prevail - ...forcedHeaders, - [MESSAGE_HEADERS.MESSAGE.SERVER_TIMESTAMP]: Date.now(), - }; + const forceOptions = pubOpts?.amqpPublishOpts + const serviceId = helper.getServiceName() + const inheritedHeaders = + originalMsg !== undefined ? originalMsg.properties.headers : {} + const forcedHeaders = forceOptions !== undefined ? forceOptions.headers : {} + + return _.merge({ + // set ORIGIN_SERVICE_ID but let it be overriden by inheritedHeaders if exists there + [MESSAGE_HEADERS.MESSAGE.ORIGIN_SERVICE_ID]: helper.getServiceName(), + ...inheritedHeaders, + // force override these default headers + [MESSAGE_HEADERS.MESSAGE.CURRENT_SERVICE_ID]: serviceId, + [MESSAGE_HEADERS.MESSAGE.INSTANCE_ID]: helper.getHostName(), + [MESSAGE_HEADERS.MESSAGE.EVENT_TYPE]: exchangeName, + // manually passed options should prevail + ...forcedHeaders, + [MESSAGE_HEADERS.MESSAGE.SERVER_TIMESTAMP]: Date.now(), + }) } function validateBeforePublish( - msgMeta: message.ProcessedMessageMetadataI, - options?: amqplib.Options.Publish, + msgMeta: message.ProcessedMessageMetadataI, + options?: amqplib.Options.Publish, + overrideScope?: message.Scope, ): void { - const { scope } = msgMeta.processedConfig; - if (scope === message.Scope.FRONTEND) { - throw new Error("ERR_IRIS_PUBLISH_TO_FRONTENT_SCOPE_NOT_SUPPORTED"); - } - if (scope === message.Scope.USER) { - const userId = ( - options?.headers[MESSAGE_HEADERS.MESSAGE.USER_ID] - ); - if (userId === undefined) { - throw new Error("ERR_IRIS_PUBLISH_TO_USER_SCOPE_WITHOUT_USER_ID"); - } - } else if (scope === message.Scope.SESSION) { - const sessionId = ( - options?.headers[MESSAGE_HEADERS.MESSAGE.SESSION_ID] - ); - if (sessionId === undefined) { - throw new Error("ERR_IRIS_PUBLISH_TO_SESSION_SCOPE_WITHOUT_SESSION_ID"); - } - } + const scope = overrideScope ?? msgMeta.processedConfig.scope + if (scope === message.Scope.FRONTEND) { + throw new Error('ERR_IRIS_PUBLISH_TO_FRONTENT_SCOPE_NOT_SUPPORTED') + } + if (scope === message.Scope.USER) { + const userId = ( + options?.headers[MESSAGE_HEADERS.MESSAGE.USER_ID] + ) + if (userId === undefined) { + throw new Error('ERR_IRIS_PUBLISH_TO_USER_SCOPE_WITHOUT_USER_ID') + } + } else if (scope === message.Scope.SESSION) { + const sessionId = ( + options?.headers[MESSAGE_HEADERS.MESSAGE.SESSION_ID] + ) + if (sessionId === undefined) { + throw new Error('ERR_IRIS_PUBLISH_TO_SESSION_SCOPE_WITHOUT_SESSION_ID') + } + } } diff --git a/src/lib/register.processed.ts b/src/lib/register.processed.ts index ebe6a47..3bc1fad 100644 --- a/src/lib/register.processed.ts +++ b/src/lib/register.processed.ts @@ -1,144 +1,149 @@ -import * as amqplib from "amqplib"; -import { getLogger } from "../logger"; -import * as amqpHelper from "./amqp.helper"; -import { connection } from "./connection"; -import { MANAGED_EXCHANGES } from "./constants"; -import * as consume from "./consume"; -import * as featManagement from "./feat.management"; -import * as message from "./message"; -import * as messageHandler from "./message_handler"; -import * as reinitialize from "./register.reinitialize"; - -const { DEAD_LETTER, FRONTEND } = MANAGED_EXCHANGES; -const logger = getLogger("Iris:RegisterProcessed"); +import type * as amqplib from 'amqplib' +import logger from '../logger' +import * as amqpHelper from './amqp.helper' +import { connection } from './connection' +import { MANAGED_EXCHANGES } from './constants' +import * as consume from './consume' +import * as featManagement from './feat.management' +import * as message from './message' +import type * as messageHandler from './message_handler' +import * as reinitialize from './register.reinitialize' + +const { DEAD_LETTER, FRONTEND } = MANAGED_EXCHANGES + +const TAG = 'Iris:RegisterProcessed' export async function register( - messages: message.ProcessedMessageMetadataI[], - messageHandlers: messageHandler.ProcessedMessageHandlerMetadataI[], + messages: message.ProcessedMessageMetadataI[], + messageHandlers: messageHandler.ProcessedMessageHandlerMetadataI[], ): Promise { - for (const msgMeta of messages) { - await assertExchangeAndQueues(msgMeta, messageHandlers); - } + for (const msgMeta of messages) { + await assertExchangeAndQueues(msgMeta, messageHandlers) + } } async function assertExchangeAndQueues( - msgMeta: message.ProcessedMessageMetadataI, - messageHandlers: messageHandler.ProcessedMessageHandlerMetadataI[], + msgMeta: message.ProcessedMessageMetadataI, + messageHandlers: messageHandler.ProcessedMessageHandlerMetadataI[], ): Promise { - await assertExchange(msgMeta); - const handlers = messageHandlers.filter( - (mh) => mh.messageClass === msgMeta.target, - ); - for (const handler of handlers) { - const obtainChannel = async (): Promise => - connection.assureChannelForHandler(handler); - const channel = await obtainChannel(); - const reinit = reinitialize.getReinitializationFn(async () => { - channel.emit("iris:reinit"); - await assertExchangeAndQueues(msgMeta, messageHandlers); - }); - - if (message.isFrontend(msgMeta)) { - await registerFrontendMessageHandler(handler, msgMeta); - } else { - await registerMessageHandler(handler, msgMeta); - } - - await consume.registerConsumer(handler, msgMeta, obtainChannel, reinit); - } + await assertExchange(msgMeta) + + const handlers = messageHandlers.filter( + (mh) => mh.messageClass === msgMeta.target, + ) + for (const handler of handlers) { + const obtainChannel = async (): Promise => + connection.assureChannelForHandler(handler) + const channel = await obtainChannel() + const reinit = reinitialize.getReinitializationFn(async () => { + channel.emit('iris:reinit') + await assertExchangeAndQueues(msgMeta, messageHandlers) + }) + + if (message.isFrontend(msgMeta)) { + await registerFrontendMessageHandler(handler, msgMeta) + } else { + await registerMessageHandler(handler, msgMeta) + } + + await consume.registerConsumer(handler, msgMeta, obtainChannel, reinit) + } } async function assertExchange( - msg: message.ProcessedMessageMetadataI, + msg: message.ProcessedMessageMetadataI, ): Promise { - if (message.isFrontend(msg)) { - await initFrontendQueue(); - } else { - await amqpHelper.assertExchange( - msg.processedConfig.exchangeName, - msg.processedConfig.exchangeType, - msg.processedConfig.exchangeOptions, - ); - } + if (message.isFrontend(msg)) { + await initFrontendQueue() + } else if (msg.processedConfig.doAssertExchange) { + await amqpHelper.assertExchange( + msg.processedConfig.exchangeName, + msg.processedConfig.exchangeType, + msg.processedConfig.exchangeOptions, + ) + } } async function registerMessageHandler( - handler: messageHandler.ProcessedMessageHandlerMetadataI, - msgMeta: message.ProcessedMessageMetadataI, + handler: messageHandler.ProcessedMessageHandlerMetadataI, + msgMeta: message.ProcessedMessageMetadataI, ): Promise { - const channel = await connection.assureChannelForHandler(handler); - const { exchange, queueName, queueOptions, bindingKeys } = - handler.processedConfig; - - await registerDeadletter(handler, msgMeta); - await amqpHelper.assertQueue(queueName, queueOptions); - - logger.log(`Bind ${msgMeta.processedConfig.exchangeType} queue to exchange`, { - queueName, - exchange, - targetClassName: msgMeta.targetClassName, - bindingKeys, - }); - - for (const bindKey of bindingKeys) { - await channel.bindQueue(queueName, exchange, bindKey); - } + const channel = await connection.assureChannelForHandler(handler) + const { exchange, queueName, queueOptions, bindingKeys } = + handler.processedConfig + + await registerDeadletter(handler, msgMeta) + await amqpHelper.assertQueue(queueName, queueOptions) + + logger.debug( + TAG, + `Bind ${msgMeta.processedConfig.exchangeType} queue to exchange`, + { + queueName, + exchange, + targetClassName: msgMeta.targetClassName, + bindingKeys, + }, + ) + + for (const bindKey of bindingKeys) { + await channel.bindQueue(queueName, exchange, bindKey) + } } async function registerFrontendMessageHandler( - handler: messageHandler.ProcessedMessageHandlerMetadataI, - msgMeta: message.ProcessedMessageMetadataI, + handler: messageHandler.ProcessedMessageHandlerMetadataI, + msgMeta: message.ProcessedMessageMetadataI, ): Promise { - const fronendQueueName = amqpHelper.getFrontendQueueName(); - const channel = await connection.assureChannelForHandler(handler); - const { routingKey } = msgMeta.processedConfig; - logger.log(`Bind ${fronendQueueName} queue to FRONTEND exchange`, { - routingKey, - fronendQueueName, - targetClassName: msgMeta.targetClassName, - }); - - await channel.bindQueue(fronendQueueName, FRONTEND.EXCHANGE, routingKey); + const frontendQueueName = amqpHelper.getFrontendQueueName() + const channel = await connection.assureChannelForHandler(handler) + const { routingKey } = msgMeta.processedConfig + logger.debug(TAG, `Bind ${frontendQueueName} queue to FRONTEND exchange`, { + routingKey, + frontendQueueName, + targetClassName: msgMeta.targetClassName, + }) + + await channel.bindQueue(frontendQueueName, FRONTEND.EXCHANGE, routingKey) } async function registerDeadletter( - handler: messageHandler.ProcessedMessageHandlerMetadataI, - msgMeta: message.ProcessedMessageMetadataI, + handler: messageHandler.ProcessedMessageHandlerMetadataI, + msgMeta: message.ProcessedMessageMetadataI, ): Promise { - const { deadLetterIsCustom } = msgMeta.processedConfig; - const { deadLetterExchange, deadLetterRoutingKey } = - handler.processedConfig.queueOptions; - - if ( - !deadLetterIsCustom || - deadLetterExchange === undefined || - deadLetterRoutingKey === undefined - ) { - return; - } - - logger.debug( - `Register custom DQL for '${handler.processedConfig.queueName}'`, - { - ...handler.processedConfig.queueOptions, - }, - ); - - await amqpHelper.assertExchange( - deadLetterExchange, - DEAD_LETTER.EXCHANGE_TYPE, - DEAD_LETTER.EXCHANGE_OPTIONS, - ); - await amqpHelper.assertQueue(deadLetterExchange, DEAD_LETTER.QUEUE_OPTIONS); - - const channel = await connection.assureChannelForHandler(handler); - await channel.bindQueue(deadLetterExchange, deadLetterExchange, "#"); + const { deadLetterIsCustom } = msgMeta.processedConfig + const { deadLetterExchange, deadLetterRoutingKey } = + handler.processedConfig.queueOptions + + if ( + !deadLetterIsCustom || + deadLetterExchange === undefined || + deadLetterRoutingKey === undefined + ) { + return + } + + logger.debug( + TAG, + `Register custom DQL for '${handler.processedConfig.queueName}'`, + { options: handler.processedConfig.queueOptions }, + ) + + await amqpHelper.assertExchange( + deadLetterExchange, + DEAD_LETTER.EXCHANGE_TYPE, + DEAD_LETTER.EXCHANGE_OPTIONS, + ) + await amqpHelper.assertQueue(deadLetterExchange, DEAD_LETTER.QUEUE_OPTIONS) + + const channel = await connection.assureChannelForHandler(handler) + await channel.bindQueue(deadLetterExchange, deadLetterExchange, '#') } async function initFrontendQueue(): Promise { - const fronendQueueName = amqpHelper.getFrontendQueueName(); - await Promise.all([ - featManagement.registerFrontendExchange(), - amqpHelper.assertQueue(fronendQueueName, FRONTEND.QUEUE_OPTIONS), - ]); + const frontendQueueName = amqpHelper.getFrontendQueueName() + await Promise.all([ + featManagement.registerFrontendExchange(), + amqpHelper.assertQueue(frontendQueueName, FRONTEND.QUEUE_OPTIONS), + ]) } diff --git a/src/lib/register.reinitialize.ts b/src/lib/register.reinitialize.ts index e618f18..1c6c27c 100644 --- a/src/lib/register.reinitialize.ts +++ b/src/lib/register.reinitialize.ts @@ -1,46 +1,48 @@ -import { getLogger } from "../logger"; -import { connection } from "./connection"; -import * as constants from "./constants"; +import logger from '../logger' +import { connection } from './connection' +import * as constants from './constants' -const logger = getLogger("Iris:RegisterReinitalize"); +const TAG = 'Iris:RegisterReinitalize' -type callbackFnT = () => Promise; +type callbackFnT = () => Promise export function getReinitializationFn(callback: callbackFnT): () => void { - return (): void => { - process.nextTick(() => { - if (!shouldReinitalize()) { - return; - } - runReinitialization(callback); - }); - }; + return (): void => { + process.nextTick(() => { + if (!shouldReinitalize()) { + return + } + runReinitialization(callback) + }) + } } function runReinitialization(callback: callbackFnT): void { - logger.log( - `Reinitialization scheduled after ${constants.getReinitializationDelay()}ms`, - ); - setTimeout(() => { - if (!shouldReinitalize()) { - return; - } - - logger.log("Reinitializing"); - callback().catch((e) => { - logger.error("Reinitialization failed", e); - }); - }, constants.getReinitializationDelay()); + logger.debug( + TAG, + `Reinitialization scheduled after ${constants.getReinitializationDelay()}ms`, + ) + setTimeout(() => { + if (!shouldReinitalize()) { + return + } + + logger.debug(TAG, 'Reinitializing') + callback().catch((err) => { + logger.error(TAG, 'Reinitialization failed', { err }) + }) + }, constants.getReinitializationDelay()) } function shouldReinitalize(): boolean { - if (!connection.shouldAutoReconnect()) { - logger.verbose( - "Reinitialization: connection purposefully closed, not reinitializing.", - ); + if (!connection.shouldAutoReconnect()) { + logger.debug( + TAG, + 'Reinitialization: connection purposefully closed, not reinitializing.', + ) - return false; - } + return false + } - return true; + return true } diff --git a/src/lib/storage.ts b/src/lib/storage.ts index 2e69039..3aee898 100644 --- a/src/lib/storage.ts +++ b/src/lib/storage.ts @@ -1,36 +1,37 @@ -export type CallbackMethod = (msg: T) => Promise; -export type CustomDecorator = ClassDecorator & { KEY: T }; +export type CallbackMethod = (msg: T) => Promise +export type CustomDecorator = ClassDecorator & { KEY: T } -export const IRIS_MESSAGE = "iris_msg_meta"; -export const IRIS_MESSAGE_HANDLERS_META = "iris_msg_handlers_meta"; -export const IRIS_MESSAGE_HANDLERS = "iris_msg_handlers"; -export const AMQP_MESSAGE_CLASS = "amqp_message_class"; +export const IRIS_MESSAGE = 'iris_msg_meta' +export const IRIS_MESSAGE_HANDLERS_META = 'iris_msg_handlers_meta' +export const IRIS_MESSAGE_HANDLERS = 'iris_msg_handlers' +export const AMQP_MESSAGE_CLASS = 'amqp_message_class' +export const MDC_CLASS = 'mdc_class' -const storage: Object[] = []; +const storage: Object[] = [] export function registerMessage(msg: T): void { - storage.push(msg); + storage.push(msg) } export function getMessageStore(): T[] { - return storage; + return storage } export const SetMetadata = ( - metadataKey: K, - metadataValue: V, + metadataKey: K, + metadataValue: V, ): CustomDecorator => { - const decoratorFactory = ( - target: TFunction, - ): TFunction | void => { - Reflect.defineMetadata(metadataKey, metadataValue, target); + const decoratorFactory = ( + target: TFunction, + ): TFunction | undefined => { + Reflect.defineMetadata(metadataKey, metadataValue, target) - return target; - }; - decoratorFactory.KEY = metadataKey; + return target + } + decoratorFactory.KEY = metadataKey - return decoratorFactory; -}; + return decoratorFactory +} export const clearMessageStore = (): void => { - storage.length = 0; -}; + storage.length = 0 +} diff --git a/src/lib/subscription.decorator.ts b/src/lib/subscription.decorator.ts index d6b627e..7037385 100644 --- a/src/lib/subscription.decorator.ts +++ b/src/lib/subscription.decorator.ts @@ -1,22 +1,22 @@ -import { MessageHandler } from "./message_handler.decorator"; -import * as interfaces from "./subscription.interfaces"; +import { MessageHandler } from './message_handler.decorator' +import type * as interfaces from './subscription.interfaces' /** * AMQP queue decorator for SubscriptionMessageHandler. */ export const SnapshotMessageHandler = - ( - config: interfaces.SnapshotMessageHandlerI, - replyClass?: Object, - ): MethodDecorator => - ( - target: Object, - propertyKey: string | symbol, - descriptor: PropertyDescriptor, - ): void => { - const { resourceType, ...msgHandlerConfig } = config; - MessageHandler( - { ...msgHandlerConfig, bindingKeys: resourceType }, - replyClass, - )(target, propertyKey, descriptor); - }; + ( + config: interfaces.SnapshotMessageHandlerI, + replyClass?: Object, + ): MethodDecorator => + ( + target: Object, + propertyKey: string | symbol, + descriptor: PropertyDescriptor, + ): void => { + const { resourceType, ...msgHandlerConfig } = config + MessageHandler( + { ...msgHandlerConfig, bindingKeys: resourceType }, + replyClass, + )(target, propertyKey, descriptor) + } diff --git a/src/lib/subscription.interfaces.ts b/src/lib/subscription.interfaces.ts index 0610b2b..fc4200e 100644 --- a/src/lib/subscription.interfaces.ts +++ b/src/lib/subscription.interfaces.ts @@ -1,23 +1,23 @@ -import * as messageHandlerI from "./message_handler.interfaces"; +import type * as messageHandlerI from './message_handler.interfaces' export interface SubscriptionI { - resourceType: string; - resourceId: string; + resourceType: string + resourceId: string } export interface SubscriptionResourceUpdateI extends SubscriptionI { - payload: object; + payload: object } export type SubscriptionPublisherI = ( - msg: T, - resourceType: string, - resourceId: string, -) => Promise; + msg: T, + resourceType: string, + resourceId: string, +) => Promise export type SnapshotMessageHandlerI = Pick< - messageHandlerI.MessageHandlerI, - "prefetch" + messageHandlerI.MessageHandlerI, + 'prefetch' > & { - resourceType: string; -}; + resourceType: string +} diff --git a/src/lib/subscription.messages.ts b/src/lib/subscription.messages.ts index 163771c..b42adf8 100644 --- a/src/lib/subscription.messages.ts +++ b/src/lib/subscription.messages.ts @@ -1,14 +1,16 @@ -import { IsObject, IsString } from "class-validator"; -import { MANAGED_EXCHANGES } from "./constants"; -import { Message } from "./message.decorator"; -import * as interfaces from "./subscription.interfaces"; +import { IsObject, IsString } from 'class-validator' +import { MANAGED_EXCHANGES } from './constants' +import { Message } from './message.decorator' +import type * as interfaces from './subscription.interfaces' + +// TODO: cache ttl etc.. const { SUBSCRIPTION, SNAPSHOT_REQUESTED, SUBSCRIBE_INTERNAL } = - MANAGED_EXCHANGES; + MANAGED_EXCHANGES class Subscription implements interfaces.SubscriptionI { - @IsString() resourceType!: string; - @IsString() resourceId!: string; + @IsString() resourceType!: string + @IsString() resourceId!: string } @Message({ name: SNAPSHOT_REQUESTED.EXCHANGE }) @@ -18,12 +20,12 @@ export class SnapshotRequested extends Subscription {} export class SubscribeInternal extends Subscription {} @Message({ - name: SUBSCRIPTION.EXCHANGE, - exchangeType: SUBSCRIPTION.EXCHANGE_TYPE, + name: SUBSCRIPTION.EXCHANGE, + exchangeType: SUBSCRIPTION.EXCHANGE_TYPE, }) export class ResourceMessage - extends Subscription - implements interfaces.SubscriptionResourceUpdateI + extends Subscription + implements interfaces.SubscriptionResourceUpdateI { - @IsObject() payload!: object; + @IsObject() payload!: object } diff --git a/src/lib/subscription.ts b/src/lib/subscription.ts index 13a5fa1..c0db061 100644 --- a/src/lib/subscription.ts +++ b/src/lib/subscription.ts @@ -1,16 +1,18 @@ -import { ClassConstructor } from "class-transformer"; -import { getLogger } from "../logger"; -import * as message from "./message"; -import { AmqpMessage } from "./message_handler"; -import * as publish from "./publish"; -import * as interfaces from "./subscription.interfaces"; -import { ResourceMessage, SubscribeInternal } from "./subscription.messages"; +import type { ClassConstructor } from 'class-transformer' +import logger from '../logger' +import { asError } from './errors' +import * as message from './message' +import { AmqpMessage } from './message_handler' +import * as publish from './publish' +import type * as interfaces from './subscription.interfaces' +import { ResourceMessage, SubscribeInternal } from './subscription.messages' -export * from "./subscription.interfaces"; +export * from './subscription.interfaces' -const logger = getLogger("Iris:Subscription"); -const RESOURCE_ROUTING_POSTFIX = ".resource"; -const subscriptionPublisher = publish.getPublisher(ResourceMessage); +const TAG = 'Iris:Subscription' + +const RESOURCE_ROUTING_POSTFIX = '.resource' +const subscriptionPublisher = publish.getPublisher(ResourceMessage) /** * Subscribe client (user) to a specific resource from back-end. @@ -19,58 +21,58 @@ const subscriptionPublisher = publish.getPublisher(ResourceMessage); * @param resourceId */ export async function internallySubscribe( - sourceEvent: AmqpMessage, - resourceType: string, - resourceId: string, + sourceEvent: AmqpMessage, + resourceType: string, + resourceId: string, ): Promise { - await publish.publishReply(sourceEvent, SubscribeInternal, { - resourceId, - resourceType, - }); + await publish.publishReply(sourceEvent, SubscribeInternal, { + resourceId, + resourceType, + }) } /** * Retuns a method which can be used to publish Snapshot events */ export function getSnapshotPublisher( - messageClass: ClassConstructor, + messageClass: ClassConstructor, ): interfaces.SubscriptionPublisherI { - return async ( - msg: T, - resourceType: string, - resourceId: string, - ): Promise => { - return sendToSubscription(messageClass, msg, resourceType, resourceId); - }; + return async ( + msg: T, + resourceType: string, + resourceId: string, + ): Promise => { + return sendToSubscription(messageClass, msg, resourceType, resourceId) + } } export async function sendToSubscription( - messageClass: ClassConstructor, - msg: T, - resourceType: string, - resourceId: string, + messageClass: ClassConstructor, + msg: T, + resourceType: string, + resourceId: string, ): Promise { - const msgMeta = getMessageMetaFromClass( - messageClass, - "sendToSubscription() passed argument should be class decorated with @Message()", - ); - const { exchangeName } = msgMeta.processedConfig; - const routingKey = `${exchangeName}${RESOURCE_ROUTING_POSTFIX}`; + const msgMeta = getMessageMetaFromClass( + messageClass, + 'sendToSubscription() passed argument should be class decorated with @Message()', + ) + const { exchangeName } = msgMeta.processedConfig + const routingKey = `${exchangeName}${RESOURCE_ROUTING_POSTFIX}` - return subscriptionPublisher( - { resourceId, resourceType, payload: msg }, - { routingKey }, - ); + return subscriptionPublisher( + { resourceId, resourceType, payload: msg }, + { routingKey }, + ) } function getMessageMetaFromClass( - messageClass: ClassConstructor, - onErrMsg: string, + messageClass: ClassConstructor, + onErrMsg: string, ): message.ProcessedMessageMetadataI { - try { - return message.getProcessedMessageDecoratedClass(messageClass); - } catch (error) { - logger.error(onErrMsg, error, { messageClass }); - throw new Error("ERR_IRIS_SUBSCRIPTION_INVALID_MESSAGE_CLASS"); - } + try { + return message.getProcessedMessageDecoratedClass(messageClass) + } catch (err) { + logger.error(TAG, onErrMsg, { err: asError(err), messageClass }) + throw new Error('ERR_IRIS_SUBSCRIPTION_INVALID_MESSAGE_CLASS') + } } diff --git a/src/lib/validation.class.ts b/src/lib/validation.class.ts index 111093b..8074e3e 100644 --- a/src/lib/validation.class.ts +++ b/src/lib/validation.class.ts @@ -1,79 +1,79 @@ -import * as amqplib from "amqplib"; -import * as validationI from "class-transformer"; -import * as classValidator from "class-validator"; -import _ from "lodash"; -import * as errors from "./errors"; -import * as messageI from "./message.interfaces"; -import * as interfaces from "./validation.interfaces"; +import type * as amqplib from 'amqplib' +import * as validationI from 'class-transformer' +import * as classValidator from 'class-validator' +import _ from 'lodash' +import * as errors from './errors' +import type * as messageI from './message.interfaces' +import type * as interfaces from './validation.interfaces' -let DEFALT_VALIDATION_OPTIONS: interfaces.ValidationOptions | undefined; +let DEFALT_VALIDATION_OPTIONS: interfaces.ValidationOptions | undefined export function setDefaultValidationOptions( - options?: interfaces.ValidationOptions, + options?: interfaces.ValidationOptions, ): void { - DEFALT_VALIDATION_OPTIONS = options; + DEFALT_VALIDATION_OPTIONS = options } export async function convertBufferToTargetClass( - msg: amqplib.ConsumeMessage, - msgMeta: messageI.MessageMetadataI, - disableValidation: boolean, + msg: amqplib.ConsumeMessage, + msgMeta: messageI.MessageMetadataI, + disableValidation: boolean, ): Promise { - const parsed = parseToObject(msg.content.toString()); + const parsed = parseToObject(msg.content.toString()) - return convertToTargetClass( - parsed, - >msgMeta.target, - msgMeta.validation, - disableValidation, - ); + return convertToTargetClass( + parsed, + >msgMeta.target, + msgMeta.validation, + disableValidation, + ) } export async function convertToTargetClass( - obj: T, - targetClass: validationI.ClassConstructor, - validation: interfaces.ValidationOptions | undefined, - disableValidation: boolean, + obj: T, + targetClass: validationI.ClassConstructor, + validation: interfaces.ValidationOptions | undefined, + disableValidation: boolean, ): Promise { - const cValidation: boolean | classValidator.ValidationOptions | undefined = - validation?.validate ?? DEFALT_VALIDATION_OPTIONS?.validate; + const cValidation: boolean | classValidator.ValidationOptions | undefined = + validation?.validate ?? DEFALT_VALIDATION_OPTIONS?.validate - const cTransform: validationI.TransformOptions | undefined = - validation?.classTransform ?? DEFALT_VALIDATION_OPTIONS?.classTransform; + const cTransform: validationI.TransformOptions | undefined = + validation?.classTransform ?? DEFALT_VALIDATION_OPTIONS?.classTransform - const converted = validationI.plainToInstance( - targetClass, - obj, - cTransform, - ); + const converted = validationI.plainToInstance( + targetClass, + obj, + cTransform, + ) - const doValidate = !disableValidation && cValidation !== false; + const doValidate = !disableValidation && cValidation !== false - if (doValidate) { - const validationOpts = - typeof cValidation === "object" ? cValidation : undefined; + if (doValidate) { + const validationOpts = + typeof cValidation === 'object' ? cValidation : undefined - const validationErrors = await classValidator.validate( - >(converted), - validationOpts, - ); + const validationErrors = await classValidator.validate( + >(converted), + validationOpts, + ) - if (validationErrors.length > 0) { - throw new errors.InvalidObjectConverionError(validationErrors); - } - } + if (validationErrors.length > 0) { + throw new errors.InvalidObjectConverionError(validationErrors) + } + } - return converted; + return converted } function parseToObject(val: string): T { - try { - const parsed = JSON.parse(val); - if (_.isPlainObject(parsed)) { - return parsed; - } - throw new errors.RejectMsgError("Parsed value is not an object"); - } catch (e) { - throw new errors.RejectMsgError((e).message); - } + try { + const parsed = JSON.parse(val) + if (_.isPlainObject(parsed)) { + return parsed + } + throw new errors.RejectMsgError('Parsed value is not an object') + } catch (err) { + throw new errors.RejectMsgError(errors.asError(err).message) + } } diff --git a/src/lib/validation.interfaces.ts b/src/lib/validation.interfaces.ts index b90e91a..2253258 100644 --- a/src/lib/validation.interfaces.ts +++ b/src/lib/validation.interfaces.ts @@ -1,22 +1,21 @@ -import * as classTransformer from "class-transformer"; -import * as classValidator from "class-validator"; +import type * as classTransformer from 'class-transformer' +import type * as classValidator from 'class-validator' export interface ValidationOptions { - classTransform?: classTransformer.ClassTransformOptions; - /** - * If false, Messages will not be validated against the - * decorated class neither when publishing nor when received. - */ - validate?: boolean | classValidator.ValidatorOptions; + classTransform?: classTransformer.ClassTransformOptions + /** + * If false, Messages will not be validated against the + * decorated class neither when publishing nor when received. + */ + validate?: boolean | classValidator.ValidatorOptions } export class ValidationError extends Error { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(tag: string, msg: string, details?: Record) { - super( - `ERR_IRIS_${tag}\n${msg}${ - details === undefined ? "" : `\n${JSON.stringify(details, null, 2)}` - }`, - ); - } + constructor(tag: string, msg: string, details?: Record) { + super( + `ERR_IRIS_${tag}\n${msg}${ + details === undefined ? '' : `\n${JSON.stringify(details, null, 2)}` + }`, + ) + } } diff --git a/src/lib/validation.iris.ts b/src/lib/validation.iris.ts index f73a6ad..9d6890d 100644 --- a/src/lib/validation.iris.ts +++ b/src/lib/validation.iris.ts @@ -1,43 +1,43 @@ -import { MANAGED_EXCHANGES, getReservedNames } from "./constants"; -import flags from "./flags"; -import { ValidationError } from "./validation.interfaces"; +import { MANAGED_EXCHANGES, getReservedNames } from './constants' +import flags from './flags' +import { ValidationError } from './validation.interfaces' -const { DEAD_LETTER } = MANAGED_EXCHANGES; +const { DEAD_LETTER } = MANAGED_EXCHANGES const VALIDATION_PATTERNS = { - TOPIC_PATTERN: /^([#*]|[\da-z-]+)(\.([#*]|[\da-z-]+))*$/, - KEBAB_CASE_PATTERN: /^(((\w+)(-\w+)*)(\/))?(\w+)(-\w+)*$/, -}; + TOPIC_PATTERN: /^([#*]|[\da-z-]+)(\.([#*]|[\da-z-]+))*$/, + KEBAB_CASE_PATTERN: /^(((\w+)(-\w+)*)(\/))?(\w+)(-\w+)*$/, +} export function throwIfValueIsInvalidCaseFormat( - name: string, - errorTag: string, - nameTag: string, - useTopicPattern = false, + name: string, + errorTag: string, + nameTag: string, + useTopicPattern = false, ): void { - const valueToCheck = name.replace(DEAD_LETTER.PREFIX, ""); - const isReservedName = getReservedNames().includes(valueToCheck); + const valueToCheck = name.replace(DEAD_LETTER.PREFIX, '') + const isReservedName = getReservedNames().includes(valueToCheck) - if (isReservedName) { - if (!flags.ALLOW_USING_RESERVED_NAMES) { - throw new ValidationError( - errorTag, - `${nameTag} is is invalid, because it's using reserved word: ${valueToCheck}`, - { name }, - ); - } - return; - } + if (isReservedName) { + if (!flags.ALLOW_USING_RESERVED_NAMES) { + throw new ValidationError( + errorTag, + `${nameTag} is is invalid, because it's using reserved word: ${valueToCheck}`, + { name }, + ) + } + return + } - const pattern: keyof typeof VALIDATION_PATTERNS = useTopicPattern - ? "TOPIC_PATTERN" - : "KEBAB_CASE_PATTERN"; - const regex = VALIDATION_PATTERNS[pattern]; - if (!regex.test(valueToCheck)) { - throw new ValidationError( - errorTag, - `${nameTag} is is invalid.\nFor TOPIC messages allowed format is kebab case with additional characters: '.#*'\nFor all others it should only be kebab case.`, - { name }, - ); - } + const pattern: keyof typeof VALIDATION_PATTERNS = useTopicPattern + ? 'TOPIC_PATTERN' + : 'KEBAB_CASE_PATTERN' + const regex = VALIDATION_PATTERNS[pattern] + if (!regex.test(valueToCheck)) { + throw new ValidationError( + errorTag, + `${nameTag} is is invalid.\nFor TOPIC messages allowed format is kebab case with additional characters: '.#*'\nFor all others it should only be kebab case.`, + { name }, + ) + } } diff --git a/src/lib/validation.ts b/src/lib/validation.ts index e00c219..bcff9ea 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -1,6 +1,6 @@ -import * as validationClass from "./validation.class"; -import * as validationIris from "./validation.iris"; +import * as validationClass from './validation.class' +import * as validationIris from './validation.iris' -export { validationClass, validationIris }; +export { validationClass, validationIris } -export * from "./validation.interfaces"; +export * from './validation.interfaces' diff --git a/src/logger.ts b/src/logger.ts index 184a3d9..d99ccb9 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -1,133 +1,107 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { ClassConstructor } from "class-transformer"; -export declare type LogLevel = "log" | "error" | "warn" | "debug" | "verbose"; - -interface LoggerIncompleteI { - log(message: any, ...optionalParams: any[]): any; - error(message: any, ...optionalParams: any[]): any; - errorDetails(message: string, details?: any): any; - warn(message: any, ...optionalParams: any[]): any; - debug?(message: any, ...optionalParams: any[]): any; - verbose?(message: any, ...optionalParams: any[]): any; - setLogLevels?(levels: LogLevel[]): any; +export declare type LogLevel = 'debug' | 'log' | 'warn' | 'error' | 'silent' + +const LEVEL_BITMASK: Record = { + silent: 0, + error: 1, + warn: 2, + log: 4, + debug: 8, } -export interface LoggerI extends LoggerIncompleteI { - debug(message: any, ...optionalParams: any[]): any; - verbose(message: any, ...optionalParams: any[]): any; - setLogLevels(levels: LogLevel[]): void; +export interface LoggerI { + debug(ctx: string, message: string, additional?: any): void + log(ctx: string, message: string, additional?: any): void + warn(ctx: string, message: string, additional?: any): void + error(ctx: string, message: string, additional?: any): void } -class LoggerProxy implements LoggerI { - private logger: LoggerIncompleteI; - - constructor(logger: LoggerIncompleteI) { - this.logger = logger; - } - - public replaceLogger(logger: LoggerIncompleteI): void { - this.logger = logger; - } - - log(message: any, ...optionalParams: any[]): any { - return this.logger.log(message, ...optionalParams); - } - error(message: any, ...optionalParams: any[]): any { - return this.logger.error(message, ...optionalParams); - } - errorDetails(message: string, details?: any): any { - return this.logger.errorDetails(message, details); - } - warn(message: any, ...optionalParams: any[]): any { - return this.logger.warn(message, ...optionalParams); - } - debug(message: any, ...optionalParams: any[]): any { - if (this.logger.debug !== undefined) { - return this.logger.debug(message, ...optionalParams); - } - - return this.log(message, ...optionalParams); - } - verbose(message: any, ...optionalParams: any[]): any { - if (this.logger.verbose !== undefined) { - return this.logger.verbose(message, ...optionalParams); - } - - return this.log(message, ...optionalParams); - } - setLogLevels(levels: LogLevel[]): void { - if (this.logger.setLogLevels !== undefined) { - this.logger.setLogLevels(levels); - } - } +type AdditionalI = { + [key: string]: any + err?: Error } -class Loggers { - private loggers: Record = {}; - private loggerClass: ClassConstructor; - - constructor() { - this.loggerClass = DefaultLogger; - } - - public setLogger(loggerClass: ClassConstructor): void { - this.loggerClass = loggerClass; - for (const context in this.loggers) { - const logger = this.loggers[context]; - logger.replaceLogger(new this.loggerClass(context)); - } - } - - public getLogger(context: string): LoggerI { - const logger = new LoggerProxy(new this.loggerClass(context)); - this.loggers[context] = logger; - - return logger; - } +export class ConsoleLogger implements LoggerI { + console: Console = console + + static level = 0 + + static { + ConsoleLogger.setLogLevel('debug') + } + + debug(ctx: string, message: string, additional?: any): void { + this.callLog('debug', ctx, message, additional) + } + + log(ctx: string, message: string, additional?: any): void { + this.callLog('log', ctx, message, additional) + } + + warn(ctx: string, message: string, additional?: any): void { + this.callLog('warn', ctx, message, additional) + } + + error(ctx: string, message: string, additional?: any): void { + this.callLog('error', ctx, message, additional) + } + + private callLog( + level: LogLevel, + ctx: string, + message: string, + additional?: any, + ): void { + if (this.shouldLog(level)) { + if (additional !== undefined) { + this.console[level](`[${ctx}]`, message, additional) + } else { + this.console[level](`[${ctx}]`, message) + } + } + } + + static setLogLevel(level: LogLevel): void { + ConsoleLogger.level = Object.keys(LEVEL_BITMASK).reduce( + (acc, val) => { + if (acc.ignoreRest) return acc + if (val === level) acc.ignoreRest = true + + acc.mask |= LEVEL_BITMASK[val] + + return acc + }, + { mask: 0, ignoreRest: false }, + ).mask + } + + private shouldLog(level: LogLevel): boolean { + return (ConsoleLogger.level & LEVEL_BITMASK[level]) > 0 + } } -class DefaultLogger implements LoggerI { - console: Console; - context: string; - - constructor(componentName: string) { - this.console = console; - this.context = componentName; - } - - setContext(ctx: string): void { - this.context = ctx; - } - - error(message: string, errStack?: string | Error, details?: any): void { - this.console.error(message, { errStack, details }); - } - - errorDetails(message: string, details?: any): void { - this.console.error(message, { details }); - } - - log(message: string, details?: any): void { - this.console.info(message, { details }); - } - - warn(message: string, details?: any): void { - this.console.warn(message, { details }); - } - - debug(message: string, details?: any): void { - this.console.debug(message, { details }); - } - - verbose(message: string, details?: any): void { - this.console.debug(message, { details }); - } - - setLogLevels(): void {} +class LoggerProxy implements LoggerI { + private static logger: LoggerI = new ConsoleLogger() + + public static replaceLogger(logger: LoggerI): void { + LoggerProxy.logger = logger + } + + replaceLogger(logger: LoggerI): void { + LoggerProxy.replaceLogger(logger) + } + + debug(ctx: string, message: string, additional?: AdditionalI): void { + LoggerProxy.logger.debug(ctx, message, additional) + } + log(ctx: string, message: string, additional?: AdditionalI): void { + LoggerProxy.logger.log(ctx, message, additional) + } + warn(ctx: string, message: string, additional?: AdditionalI): void { + LoggerProxy.logger.warn(ctx, message, additional) + } + error(ctx: string, message: string, additional?: AdditionalI): void { + LoggerProxy.logger.error(ctx, message, additional) + } } -export const loggers = new Loggers(); -export const getLogger = (context: string): LoggerI => - loggers.getLogger(context); -export const setLogger = (loggerClass: ClassConstructor): void => - loggers.setLogger(loggerClass); +export default new LoggerProxy() diff --git a/src/testing/index.ts b/src/testing/index.ts new file mode 100644 index 0000000..716099f --- /dev/null +++ b/src/testing/index.ts @@ -0,0 +1,5 @@ +/* [Testing] test integration helpers */ +export * as integration from './test.integration' + +/* [Testing] test utility helpers */ +export * as utilities from './test.utilities' diff --git a/src/testing/test.integration.ts b/src/testing/test.integration.ts new file mode 100644 index 0000000..8624d3b --- /dev/null +++ b/src/testing/test.integration.ts @@ -0,0 +1,158 @@ +/** + * Test intergration for iris + */ + +import * as iris from '..' +import { isAmqpMessageClass } from '../lib/message_handler.param.decorator' +import { clearQueues, connect, deleteExchange } from './test.utilities' + +export type RegisterAndConnectReturnT = Awaited< + ReturnType +> + +export async function registerAndConnect( + handlerClasses: { new (): T[number] } | { new (): T[number] }[], + connectOptions?: iris.ConnectionConfigI, +) { + await connect(connectOptions) + + const { handlers, instanceMap } = await registerHandlers(handlerClasses) + + return { + getHandlers: () => handlers, + getHandlerFor: (handlerClass: { new (): T }): T | undefined => + instanceMap[handlerClass.name]?.handlerInstance, + deleteQueues: async (allowReconnect = false) => + await clearQueues(handlers, true, allowReconnect), + clearQueues: async (allowReconnect = false) => + await clearQueues(handlers, false, allowReconnect), + deleteExchange: async (exchangeOrMsgClass: string | Object) => { + const exchange = + typeof exchangeOrMsgClass === 'string' + ? exchangeOrMsgClass + : iris.message.getProcessedMessageDecoratedClass(exchangeOrMsgClass) + .processedConfig.exchangeName + + await deleteExchange(exchange) + }, + } +} + +export async function registerHandlers( + handlerClasses: { new (): T[number] } | { new (): T[number] }[], +) { + const instanceMap = instantiateHandlers(handlerClasses) + + const handlers = Object.values(instanceMap).flatMap(({ metas }) => metas) + await iris.featManagement.registerReservedManagedMessages() + + await iris.registerProcessed.register( + iris.collectProcessedMessages(), + handlers, + ) + + return { handlers, instanceMap } +} + +/** + * [Testing] + * @internal + * use {@link registerAndConnect} instead + * Create instances of passed handler classes and + * route events to their respective @MessageHandler methods + */ +export function instantiateHandlers( + handlerClasses: { new (): T } | { new (): T }[], +) { + return (Array.isArray(handlerClasses) ? handlerClasses : [handlerClasses]) + .map(instantiateHandler) + .reduce( + (acc, instanceAndMeta) => { + // @ts-ignore + acc[instanceAndMeta.handlerInstance.constructor.name] = instanceAndMeta + + return acc + }, + < + Record< + string, + { handlerInstance: T; metas: iris.ProcessedMessageHandlerMetadataI[] } + > + >{}, + ) +} + +function instantiateHandler(handlerClass: { new (): T }) { + const metas = iris.getProcessedMessageHandlerDecoratedMethods(handlerClass) + const handlerInstance = new handlerClass() + + for (const meta of metas) { + const msgToArgs = msgToArgsFactory(meta, meta.target, meta.methodName) + + // @ts-ignore + meta.callback = async (msg: iris.AmqpMessage) => { + return meta.isStaticMethod + ? handlerClass[meta.methodName](...(await msgToArgs(msg))) + : handlerInstance[meta.methodName](...(await msgToArgs(msg))) + } + } + + return { handlerInstance, metas } +} + +function msgToArgsFactory( + meta: iris.ProcessedMessageHandlerMetadataI, + handlerClass: Object, + methodName: string, +) { + const argFactories: Function[] = [] + const methodArgs = Reflect.getMetadata( + 'design:paramtypes', + handlerClass, + methodName, + ) + + for (const [pos, arg] of methodArgs.entries()) { + if (iris.message.isMessageDecoratedClass(arg)) { + argFactories[pos] = getMessageFactory(meta.messageClass) + } else if (isAmqpMessageClass(arg)) { + argFactories[pos] = (i: any) => i + } else if (iris.mdc.isMDCClass(arg)) { + argFactories[pos] = iris.mdc.amqpToMDC + } else { + throwTestHandlerArgError(handlerClass, methodName, arg) + } + } + + return async (msg: iris.AmqpMessage) => + await Promise.all(argFactories.map((f) => f(msg))) +} + +function getMessageFactory(msgClass: Object) { + const msgMeta = iris.message.getMessageDecoratedClass(msgClass) + + return async (msg: iris.AmqpMessage) => { + return await iris.validation.validationClass.convertBufferToTargetClass( + msg, + msgMeta, + iris.flags.DISABLE_MESSAGE_CONSUME_VALIDATION, + ) + } +} + +function throwTestHandlerArgError( + handlerClass: Object, + methodName: string, + arg: unknown, +): never { + // @ts-ignore + const desc = `${handlerClass.constructor.name}.${methodName}` + console.error( + `test integration only allows for + @Message and/or AmqpMessage arguments in handlers: + ${desc}( .. ${arg} ? ..) + `, + ) + + throw new Error(`TEST_ERR_INVALID_HANDLER_ARG ${desc}`) +} diff --git a/src/testing/test.utilities.ts b/src/testing/test.utilities.ts new file mode 100644 index 0000000..6a0efff --- /dev/null +++ b/src/testing/test.utilities.ts @@ -0,0 +1,210 @@ +import { randomUUID } from 'node:crypto' +import { nextTick } from 'node:process' +import amqplib from 'amqplib' +import * as iris from '..' +import { ConsoleLogger, LogLevel } from '../logger' + +export async function setLogLevel(level?: LogLevel) { + ConsoleLogger.setLogLevel(level ?? process.env.LOG_LEVEL ?? ('debug' as any)) +} + +export async function connect(opts?: iris.ConnectionConfigI) { + await iris.connection.connect( + opts ?? { urlOrOpts: process.env.AMQP_URL }, + ) +} + +/** + * [Testing] + * try clear all queues for passed handlers metadata + * obtained via {@link iris.getProcessedMessageDecoratedClass} + */ +export async function clearQueues( + handlers: iris.ProcessedMessageHandlerMetadataI[], + alsoDelete: boolean, + allowReconnect = false, +) { + iris.connection.setDoAutoReconnect(allowReconnect) + await Promise.all( + collectHandlerQueueInfo(handlers).map(async ({ handler, queues }) => { + let ch: amqplib.Channel | undefined + async function setupChannel() { + ch = await iris.connection.assureChannelForHandler(handler) + ch.on('error', setupChannel) + } + await setupChannel() + await nextTickP() + for (const queue of queues) { + await ch!.purgeQueue(queue).catch(nextTickP) + if (alsoDelete) { + await ch!.deleteQueue(queue).catch(nextTickP) + } + } + }), + ) + await nextTickP() +} + +/** + * [Testing] + */ +export async function deleteExchange(exchange: string) { + const ch = await getTestChannel() + await ch.deleteExchange(exchange) +} + +/** + * [Testing] + * Allow publishing to fronend exchange + */ +export async function publishToFrontend( + messageClass: { new (): T }, + message: T, + routingKey?: string, + options?: amqplib.Options.Publish, +): Promise { + const msgMeta = iris.getProcessedMessageDecoratedClass(messageClass) + const { scope, publishingExchangeName, publishingExchangeRoutingKey } = + msgMeta.processedConfig + + if (scope !== iris.Scope.FRONTEND) { + throw new Error('ERR_IRIS_TEST_INVALID_FRONTEND_MESSAGE') + } + + const rKey = + publishingExchangeRoutingKey ?? + routingKey ?? + msgMeta.processedConfig.routingKey + + const channel = await getTestChannel() + + channel.publish( + publishingExchangeName, + rKey, + Buffer.from(JSON.stringify(message)), + options, + ) +} + +/** + * Subscribe and consume any event decorated with @Message() + * @param {Object} messageClass which event to subscribe to + * @param {Function} handler (spy) + * @param {string} [bindKey='#'] which binding key to use in case of direct/topic exchange type + * + * @Returns unsubscribe method + */ +export async function subscribe( + messageClass: { new (): T }, + handler: Function, + bindKey?: string, +) { + const msgMeta = iris.getProcessedMessageDecoratedClass(messageClass) + + if (msgMeta.processedConfig.scope !== iris.Scope.INTERNAL) { + await assureSubscriptionExchange(msgMeta) + } + + // for tests we want to subscribe to exchanges where msg + // is published to based on SCOPE + const { publishingExchangeName, publishingExchangeRoutingKey } = + msgMeta.processedConfig + const bindingKey: string = bindKey ?? publishingExchangeRoutingKey ?? '' + + const qName = `${publishingExchangeName}_test_${randomUUID()}` + let channel = await getTestChannel() + await channel.assertQueue(qName, { durable: false, autoDelete: true }) + await channel.bindQueue(qName, publishingExchangeName, bindingKey) + + const cTag = await channel.consume( + qName, + // eslint-disable-next-line @typescript-eslint/no-misused-promises + async (msg: amqplib.ConsumeMessage | null) => { + if (msg !== null) { + const content = JSON.parse(msg.content.toString()) + await handler(content, msg) + } + }, + { noAck: true }, + ) + + return async (): Promise => { + channel = await getTestChannel() + await Promise.all([ + channel.unbindQueue(qName, publishingExchangeName, bindingKey), + channel.cancel(cTag.consumerTag), + ]) + } +} + +/** + * [Testing] + * Request snapshot for resource + */ +export async function requestSnapshot( + resourceType: string, + resourceId: string, +): Promise { + await iris.publish.getPublisher(iris.SnapshotRequested)( + { resourceType, resourceId }, + { routingKey: resourceType }, + ) +} + +/** + * [Testing] + * Open a amqp channel for test + */ +export async function getTestChannel(): Promise { + return await iris.connection.assureChannel('test') +} + +/** + * [Testing] + * Get a dedicated channel for for a specific Message class + */ +export async function getChannelForMessage(messageClass: { new (): T }) { + const handler = iris.getHandlers(messageClass).pop() + const handlerMeta = iris + .getProcessedMessageHandlerDecoratedMethods(handler!) + .find((h) => h.messageClass === messageClass) + + return await iris.connection.assureChannelForHandler(handlerMeta!) +} + +function collectHandlerQueueInfo( + handlers: iris.ProcessedMessageHandlerMetadataI[], +) { + return handlers.map((handler) => { + const msgMeta = iris.message.getProcessedMessageDecoratedClass( + handler.messageClass, + ) + + const queues = [handler.processedConfig.queueName] + if ( + msgMeta.processedConfig.deadLetterIsCustom && + handler.processedConfig.queueOptions.deadLetterExchange + ) { + queues.push(handler.processedConfig.queueOptions.deadLetterExchange) + } + + return { handler, queues } + }) +} + +async function assureSubscriptionExchange( + msgMeta: iris.ProcessedMessageMetadataI, +) { + await iris.amqpHelper.assertExchange( + msgMeta.processedConfig.publishingExchangeName, + msgMeta.processedConfig.exchangeType, + { + autoDelete: true, + durable: false, + }, + ) +} + +async function nextTickP() { + return new Promise((resolve) => nextTick(resolve)) +} diff --git a/test/e2e/asyncapi/__snapshots__/inventory.spec.ts.snap b/test/e2e/asyncapi/__snapshots__/inventory.spec.ts.snap new file mode 100644 index 0000000..efd8421 --- /dev/null +++ b/test/e2e/asyncapi/__snapshots__/inventory.spec.ts.snap @@ -0,0 +1,1550 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Asyncapi generation with "inventory" app > generates expected asyncapi 1`] = ` +{ + "asyncapi": "2.6.0", + "channels": { + "inventory-stock-inquiry-auth/iris-node-tests.inventory-stock-inquiry-auth": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "inventory-stock-inquiry-auth", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": false, + "durable": false, + "exclusive": false, + "messageTtl": 15000, + "name": "iris-node-tests.inventory-stock-inquiry-auth", + "vhost": "/", + }, + }, + }, + "publish": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "FRONTEND", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + 15000, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "AuthenticatedInventoryStockInquiry", + "payload": { + "$ref": "#/components/schemas/AuthenticatedInventoryStockInquiry", + }, + "title": "AuthenticatedInventoryStockInquiry", + "x-response": { + "$ref": "#/components/schemas/InventoryStock", + }, + }, + }, + }, + "inventory-stock-inquiry-client-exception/iris-node-tests.inventory-stock-inquiry-client-exception": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "inventory-stock-inquiry-client-exception", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": false, + "durable": false, + "exclusive": false, + "messageTtl": 15000, + "name": "iris-node-tests.inventory-stock-inquiry-client-exception", + "vhost": "/", + }, + }, + }, + "publish": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "FRONTEND", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + 15000, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "ClientExceptionalInventoryStockInquiry", + "payload": { + "$ref": "#/components/schemas/ClientExceptionalInventoryStockInquiry", + }, + "title": "ClientExceptionalInventoryStockInquiry", + "x-response": { + "$ref": "#/components/schemas/InventoryStock", + }, + }, + }, + }, + "inventory-stock-inquiry-server-exception/iris-node-tests.inventory-stock-inquiry-server-exception": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "inventory-stock-inquiry-server-exception", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": false, + "durable": false, + "exclusive": false, + "messageTtl": 15000, + "name": "iris-node-tests.inventory-stock-inquiry-server-exception", + "vhost": "/", + }, + }, + }, + "publish": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "FRONTEND", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + 15000, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "ServerExceptionalInventoryStockInquiry", + "payload": { + "$ref": "#/components/schemas/ServerExceptionalInventoryStockInquiry", + }, + "title": "ServerExceptionalInventoryStockInquiry", + "x-response": { + "$ref": "#/components/schemas/InventoryStock", + }, + }, + }, + }, + "inventory-stock-inquiry-silent-server-exception/iris-node-tests.inventory-stock-inquiry-silent-server-exception": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "inventory-stock-inquiry-silent-server-exception", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": false, + "durable": false, + "exclusive": false, + "messageTtl": 15000, + "name": "iris-node-tests.inventory-stock-inquiry-silent-server-exception", + "vhost": "/", + }, + }, + }, + "publish": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "FRONTEND", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + 15000, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "SilentServerExceptionalInventoryStockInquiry", + "payload": { + "$ref": "#/components/schemas/SilentServerExceptionalInventoryStockInquiry", + }, + "title": "SilentServerExceptionalInventoryStockInquiry", + "x-response": { + "$ref": "#/components/schemas/InventoryStock", + }, + }, + }, + }, + "inventory-stock-inquiry/iris-node-tests.inventory-stock-inquiry": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "inventory-stock-inquiry", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": false, + "durable": false, + "exclusive": false, + "messageTtl": 15000, + "name": "iris-node-tests.inventory-stock-inquiry", + "vhost": "/", + }, + }, + }, + "publish": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "FRONTEND", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + 15000, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "InventoryStockInquiry", + "payload": { + "$ref": "#/components/schemas/InventoryStockInquiry", + }, + "title": "InventoryStockInquiry", + "x-response": { + "$ref": "#/components/schemas/InventoryStock", + }, + }, + }, + }, + "inventory-stock-update/inventory-stock-update": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "inventory-stock-update", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "USER", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "InventoryStockUpdate", + "payload": { + "$ref": "#/components/schemas/InventoryStockUpdate", + }, + "title": "InventoryStockUpdate", + }, + }, + }, + "inventory-stock/inventory-stock": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "inventory-stock", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "SESSION", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "InventoryStock", + "payload": { + "$ref": "#/components/schemas/InventoryStock", + }, + "title": "InventoryStock", + }, + }, + }, + "order-finalize-request/iris-node-tests.order-finalize-request": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "order-finalize-request", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": false, + "deadLetterExchange": "dead.dead-letter", + "deadLetterRoutingKey": "dead.iris-node-tests.order-finalize-request", + "durable": true, + "exclusive": false, + "name": "iris-node-tests.order-finalize-request", + "vhost": "/", + }, + }, + }, + "publish": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "INTERNAL", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "OrderFinalizeRequest", + "payload": { + "$ref": "#/components/schemas/OrderFinalizeRequest", + }, + "title": "OrderFinalizeRequest", + "x-response": { + "$ref": "#/components/schemas/OrderFinalizeResponse", + }, + }, + }, + }, + "order-inventory-confirmation-rpc/order-inventory-confirmation-rpc": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "order-inventory-confirmation-rpc", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "INTERNAL", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "OrderInventoryConfirmationRpc", + "payload": { + "$ref": "#/components/schemas/OrderInventoryConfirmationRpc", + }, + "title": "OrderInventoryConfirmationRpc", + }, + }, + }, + "order-inventory-confirmation/order-inventory-confirmation": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "order-inventory-confirmation", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "INTERNAL", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "OrderInventoryConfirmation", + "payload": { + "$ref": "#/components/schemas/OrderInventoryConfirmation", + }, + "title": "OrderInventoryConfirmation", + }, + }, + }, + "order-inventory-lookup-rpc/iris-node-tests.order-inventory-lookup-rpc": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "order-inventory-lookup-rpc", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": false, + "deadLetterExchange": "dead.dead-letter", + "deadLetterRoutingKey": "dead.iris-node-tests.order-inventory-lookup-rpc", + "durable": true, + "exclusive": false, + "name": "iris-node-tests.order-inventory-lookup-rpc", + "vhost": "/", + }, + }, + }, + "publish": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "INTERNAL", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "OrderInventoryLookupRpc", + "payload": { + "$ref": "#/components/schemas/OrderInventoryLookupRpc", + }, + "title": "OrderInventoryLookupRpc", + "x-response": { + "$ref": "#/components/schemas/OrderInventoryConfirmationRpc", + }, + }, + }, + }, + "order-inventory-lookup/iris-node-tests.order-inventory-lookup": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "order-inventory-lookup", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": false, + "deadLetterExchange": "dead.dead-letter", + "deadLetterRoutingKey": "dead.iris-node-tests.order-inventory-lookup", + "durable": true, + "exclusive": false, + "name": "iris-node-tests.order-inventory-lookup", + "vhost": "/", + }, + }, + }, + "publish": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "INTERNAL", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "OrderInventoryLookup", + "payload": { + "$ref": "#/components/schemas/OrderInventoryLookup", + }, + "title": "OrderInventoryLookup", + "x-response": { + "$ref": "#/components/schemas/OrderInventoryConfirmation", + }, + }, + }, + }, + "order-response/order-response": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "order-response", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "INTERNAL", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "OrderFinalizeResponse", + "payload": { + "$ref": "#/components/schemas/OrderFinalizeResponse", + }, + "title": "OrderFinalizeResponse", + }, + }, + }, + "restock-inventory-frontend/iris-node-tests.restock-inventory-frontend": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "restock-inventory-frontend", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": false, + "durable": false, + "exclusive": false, + "messageTtl": 15000, + "name": "iris-node-tests.restock-inventory-frontend", + "vhost": "/", + }, + }, + }, + "publish": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "FRONTEND", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + 15000, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "RestockInventoryFe", + "payload": { + "$ref": "#/components/schemas/RestockInventoryFe", + }, + "title": "RestockInventoryFe", + }, + }, + }, + "restock-inventory/iris-node-tests.restock-inventory": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "restock-inventory", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": false, + "deadLetterExchange": "dead.dead-letter", + "deadLetterRoutingKey": "dead.iris-node-tests.restock-inventory", + "durable": true, + "exclusive": false, + "name": "iris-node-tests.restock-inventory", + "vhost": "/", + }, + }, + }, + "publish": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "INTERNAL", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "RestockInventory", + "payload": { + "$ref": "#/components/schemas/RestockInventory", + }, + "title": "RestockInventory", + }, + }, + }, + "snapshot-requested/iris-node-tests.snapshot-requested.inventory": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "snapshot-requested", + "type": "topic", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": true, + "deadLetterExchange": "dead.dead-letter", + "deadLetterRoutingKey": "dead.iris-node-tests.snapshot-requested.inventory", + "durable": true, + "exclusive": false, + "name": "iris-node-tests.snapshot-requested.inventory", + "vhost": "/", + }, + }, + }, + "publish": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "INTERNAL", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "SnapshotRequested", + "payload": { + "$ref": "#/components/schemas/SnapshotRequested", + }, + "title": "SnapshotRequested", + }, + }, + }, + "subscribe-internal/subscribe-internal": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "subscribe-internal", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "INTERNAL", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "SubscribeInternal", + "payload": { + "$ref": "#/components/schemas/SubscribeInternal", + }, + "title": "SubscribeInternal", + }, + }, + }, + "subscription-accepted/subscription-accepted": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "subscription-accepted", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "SESSION", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "SubscriptionAccepted", + "payload": { + "$ref": "#/components/schemas/SubscriptionAccepted", + }, + "title": "SubscriptionAccepted", + }, + }, + }, + "subscription/subscription": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "subscription", + "type": "topic", + "vhost": "/", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "INTERNAL", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "ResourceMessage", + "payload": { + "$ref": "#/components/schemas/ResourceMessage", + }, + "title": "ResourceMessage", + }, + }, + }, + "system-status-inventory/system-status-inventory": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "system-status-inventory", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + }, + }, + "subscribe": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "INTERNAL", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "InventorySystemStatus", + "payload": { + "$ref": "#/components/schemas/InventorySystemStatus", + }, + "title": "InventorySystemStatus", + }, + }, + }, + "system-status-order/iris-node-tests.system-status-order": { + "bindings": { + "amqp": { + "exchange": { + "autoDelete": false, + "durable": true, + "name": "system-status-order", + "type": "fanout", + "vhost": "/", + }, + "is": "routingKey", + "queue": { + "autoDelete": false, + "deadLetterExchange": "dead.dead-letter", + "deadLetterRoutingKey": "dead.iris-node-tests.system-status-order", + "durable": true, + "exclusive": false, + "name": "iris-node-tests.system-status-order", + "vhost": "/", + }, + }, + }, + "publish": { + "message": { + "headers": { + "properties": { + "x-dead-letter": { + "description": "Dead letter queue definition. Default is dead-letter", + "enum": [ + "dead.dead-letter", + ], + "type": "string", + }, + "x-roles-allowed": { + "description": "Allowed roles for this message. Default is empty", + "type": "array", + }, + "x-scope": { + "description": "Message scope. Default is INTERNAL", + "enum": [ + "INTERNAL", + ], + "type": "string", + }, + "x-ttl": { + "description": "TTL of the message. If set to -1 (default) will use brokers default.", + "enum": [ + -1, + ], + "type": "number", + }, + }, + "type": "object", + }, + "name": "OrderSystemStatus", + "payload": { + "$ref": "#/components/schemas/OrderSystemStatus", + }, + "title": "OrderSystemStatus", + }, + }, + }, + }, + "components": { + "schemas": { + "AuthenticatedInventoryStockInquiry": { + "properties": {}, + "type": "object", + "x-iris-generated": false, + }, + "ClientExceptionalInventoryStockInquiry": { + "properties": {}, + "type": "object", + "x-iris-generated": false, + }, + "InventoryStock": { + "properties": { + "inventoryStock": { + "additionalProperties": { + "type": "number", + }, + "type": "object", + }, + }, + "required": [ + "inventoryStock", + ], + "type": "object", + "x-iris-generated": true, + }, + "InventoryStockInquiry": { + "properties": {}, + "type": "object", + "x-iris-generated": false, + }, + "InventoryStockUpdate": { + "properties": { + "inventoryStock": { + "additionalProperties": { + "type": "number", + }, + "type": "object", + }, + "message": { + "type": "string", + }, + }, + "required": [ + "message", + "inventoryStock", + ], + "type": "object", + "x-iris-generated": true, + }, + "InventorySystemStatus": { + "properties": { + "inventory": { + "additionalProperties": { + "type": "number", + }, + "type": "object", + }, + "orders": { + "items": { + "type": "string", + }, + "type": "array", + }, + }, + "required": [ + "orders", + "inventory", + ], + "type": "object", + "x-iris-generated": true, + }, + "OrderFinalizeRequest": { + "properties": { + "finalize": { + "type": "boolean", + }, + }, + "required": [ + "finalize", + ], + "type": "object", + "x-iris-generated": false, + }, + "OrderFinalizeResponse": { + "properties": { + "ack": { + "type": "boolean", + }, + }, + "required": [ + "ack", + ], + "type": "object", + "x-iris-generated": true, + }, + "OrderInventoryConfirmation": { + "properties": { + "confirmed": { + "type": "boolean", + }, + "count": { + "type": "number", + }, + "lookup": { + "$ref": "#/components/schemas/OrderInventoryLookup", + }, + "orderId": { + "type": "string", + }, + "ts": { + "format": "date-time", + "type": "string", + }, + }, + "required": [ + "ts", + "orderId", + "confirmed", + "lookup", + ], + "type": "object", + "x-iris-generated": true, + }, + "OrderInventoryConfirmationRpc": { + "properties": { + "confirmed": { + "type": "boolean", + }, + "orderId": { + "type": "string", + }, + }, + "required": [ + "orderId", + "confirmed", + ], + "type": "object", + "x-iris-generated": true, + }, + "OrderInventoryLookup": { + "properties": { + "orderId": { + "type": "string", + }, + "type": { + "enum": [ + "foo", + "bar", + ], + "type": "string", + }, + }, + "required": [ + "orderId", + "type", + ], + "type": "object", + "x-iris-generated": false, + }, + "OrderInventoryLookupRpc": { + "properties": { + "items": { + "additionalProperties": { + "type": "number", + }, + "type": "object", + }, + "orderId": { + "type": "string", + }, + }, + "required": [ + "items", + "orderId", + ], + "type": "object", + "x-iris-generated": false, + }, + "OrderSystemStatus": { + "properties": { + "orders": { + "items": { + "type": "string", + }, + "type": "array", + }, + }, + "required": [ + "orders", + ], + "type": "object", + "x-iris-generated": false, + }, + "ResourceMessage": { + "properties": { + "payload": { + "type": "object", + }, + "resourceId": { + "type": "string", + }, + "resourceType": { + "type": "string", + }, + }, + "required": [ + "payload", + "resourceType", + "resourceId", + ], + "type": "object", + "x-iris-generated": true, + }, + "RestockInventory": { + "properties": { + "restockedInventory": { + "additionalProperties": { + "type": "number", + }, + "type": "object", + }, + }, + "required": [ + "restockedInventory", + ], + "type": "object", + "x-iris-generated": false, + }, + "RestockInventoryFe": { + "properties": { + "restockedInventory": { + "additionalProperties": { + "type": "number", + }, + "type": "object", + }, + }, + "required": [ + "restockedInventory", + ], + "type": "object", + "x-iris-generated": false, + }, + "ServerExceptionalInventoryStockInquiry": { + "properties": {}, + "type": "object", + "x-iris-generated": false, + }, + "SilentServerExceptionalInventoryStockInquiry": { + "properties": { + "name": { + "type": "string", + }, + }, + "required": [ + "name", + ], + "type": "object", + "x-iris-generated": false, + }, + "SnapshotRequested": { + "properties": { + "resourceId": { + "type": "string", + }, + "resourceType": { + "type": "string", + }, + }, + "required": [ + "resourceType", + "resourceId", + ], + "type": "object", + "x-iris-generated": true, + }, + "SubscribeInternal": { + "properties": { + "resourceId": { + "type": "string", + }, + "resourceType": { + "type": "string", + }, + }, + "required": [ + "resourceType", + "resourceId", + ], + "type": "object", + "x-iris-generated": true, + }, + "SubscriptionAccepted": { + "properties": { + "items": { + "items": { + "type": "string", + }, + "type": "array", + }, + }, + "required": [ + "items", + ], + "type": "object", + "x-iris-generated": true, + }, + }, + }, + "id": "urn:id:global:test_invernory", + "info": { + "contact": {}, + "description": "Test inverntory asnycapi", + "title": "test_invernory", + "version": "1.0.0", + }, + "servers": {}, + "tags": [], +} +`; diff --git a/test/e2e/asyncapi/inventory.handlers.ts b/test/e2e/asyncapi/inventory.handlers.ts new file mode 100644 index 0000000..fba8af6 --- /dev/null +++ b/test/e2e/asyncapi/inventory.handlers.ts @@ -0,0 +1,79 @@ +import { + MessageHandler, + SnapshotMessageHandler, + SnapshotRequested, +} from '../../../src' +import * as msg from './inventory.messages' + +export class HandlerInquiry { + @MessageHandler({}, msg.InventoryStock) + public inquiry(_inquiry: msg.InventoryStockInquiry): msg.InventoryStock { + return ({}) + } + + @MessageHandler({}, msg.InventoryStock) + public inquiry2( + _inquiry: msg.ClientExceptionalInventoryStockInquiry, + ): msg.InventoryStock { + return ({}) + } + + @MessageHandler({}, msg.InventoryStock) + public inquiry3( + _inquiry: msg.ServerExceptionalInventoryStockInquiry, + ): msg.InventoryStock { + return ({}) + } + + @MessageHandler({}, msg.InventoryStock) + public inquiry4( + _inquiry: msg.SilentServerExceptionalInventoryStockInquiry, + ): msg.InventoryStock { + return ({}) + } + + @MessageHandler({}, msg.InventoryStock) + public inquiry5( + _inquiry: msg.AuthenticatedInventoryStockInquiry, + ): msg.InventoryStock { + return ({}) + } +} + +export class HandlerOrder { + @MessageHandler() + public restock(_restockInventory: msg.RestockInventory): void {} + + @MessageHandler({}, msg.OrderInventoryConfirmation) + public lookupOrderInventory( + _orderInventory: msg.OrderInventoryLookup, + ): msg.OrderInventoryConfirmation { + return ({}) + } + // test handler to restock inventory without shipping service + @MessageHandler() + public restockFe(_restockInventory: msg.RestockInventoryFe): void {} + + // RPC POC message handler for inventory lookup + @MessageHandler({}, msg.OrderInventoryConfirmationRpc) + public lookupOrderInventoryRpc( + _orderInventory: msg.OrderInventoryLookupRpc, + ): msg.OrderInventoryConfirmationRpc { + return ({}) + } + + @MessageHandler({}, msg.OrderFinalizeResponse) + public finalizeOrder( + _evt: msg.OrderFinalizeRequest, + ): msg.OrderFinalizeResponse { + return { ack: true } + } +} + +export class HandlerOrderSytem { + @MessageHandler() + public systemStatus(_status: msg.OrderSystemStatus): void {} + + @SnapshotMessageHandler({ resourceType: 'inventory' }) + public snapshotRequested(_snapshotRequested: SnapshotRequested): void {} +} diff --git a/test/e2e/asyncapi/inventory.messages.ts b/test/e2e/asyncapi/inventory.messages.ts new file mode 100644 index 0000000..462cdf3 --- /dev/null +++ b/test/e2e/asyncapi/inventory.messages.ts @@ -0,0 +1,134 @@ +import { Type } from 'class-transformer' +import { + IsBoolean, + IsDate, + IsEnum, + IsInstance, + IsNumber, + IsOptional, + IsString, +} from 'class-validator' +import { Message, Scope } from '../../../src' + +enum LookupType { + foo = 'foo', + bar = 'bar', +} + +@Message({ name: 'inventory-stock-inquiry', scope: Scope.FRONTEND }) +export class InventoryStockInquiry {} + +@Message({ name: 'inventory-stock-inquiry-auth', scope: Scope.FRONTEND }) +export class AuthenticatedInventoryStockInquiry {} + +@Message({ + name: 'inventory-stock-inquiry-client-exception', + scope: Scope.FRONTEND, +}) +export class ClientExceptionalInventoryStockInquiry {} + +@Message({ + name: 'inventory-stock-inquiry-server-exception', + scope: Scope.FRONTEND, +}) +export class ServerExceptionalInventoryStockInquiry {} + +export class SilentServerExceptionalInventoryStockInquiryBase { + @IsString() name!: string +} + +@Message({ + name: 'inventory-stock-inquiry-silent-server-exception', + scope: Scope.FRONTEND, +}) +export class SilentServerExceptionalInventoryStockInquiry extends SilentServerExceptionalInventoryStockInquiryBase {} + +@Message({ name: 'subscription-accepted', scope: Scope.SESSION }) +export class SubscriptionAccepted { + @IsString({ each: true }) items!: Set +} + +@Message({ name: 'restock-inventory' }) +export class RestockInventory { + @Type(() => Number) + @IsNumber({ allowNaN: false, allowInfinity: false }, { each: true }) + restockedInventory!: Map +} + +@Message({ name: 'order-inventory-lookup' }) +export class OrderInventoryLookup { + @IsString() orderId!: string + @IsEnum(LookupType) type!: LookupType +} + +@Message({ name: 'order-inventory-confirmation' }) +export class OrderInventoryConfirmation { + @IsOptional() @IsNumber() count?: number + @IsDate() ts!: Date + @IsString() orderId!: string + @IsBoolean() confirmed!: string + @Type(() => OrderInventoryLookup) + @IsInstance(OrderInventoryLookup) + lookup!: OrderInventoryLookup +} + +@Message({ name: 'inventory-stock', scope: Scope.SESSION }) +export class InventoryStock { + @Type(() => Number) + @IsNumber({ allowNaN: false, allowInfinity: false }, { each: true }) + inventoryStock!: Map +} + +@Message({ name: 'inventory-stock-update', scope: Scope.USER }) +export class InventoryStockUpdate { + @IsString() message!: string + @Type(() => Number) + @IsNumber({ allowNaN: false, allowInfinity: false }, { each: true }) + inventoryStock!: Map +} + +@Message({ name: 'system-status-inventory' }) +export class InventorySystemStatus { + @IsString({ each: true }) orders!: Set + @Type(() => Number) + @IsNumber({ allowNaN: false, allowInfinity: false }, { each: true }) + inventory!: Map +} + +@Message({ name: 'system-status-order' }) +export class OrderSystemStatus { + @IsString({ each: true }) orders!: Set +} + +// test event for restocking the inventory from client +@Message({ name: 'restock-inventory-frontend', scope: Scope.FRONTEND }) +export class RestockInventoryFe { + @Type(() => Number) + @IsNumber({ allowNaN: false, allowInfinity: false }, { each: true }) + restockedInventory!: Map +} + +// sample events for resting RPC proof of concept +@Message({ name: 'order-inventory-lookup-rpc' }) +export class OrderInventoryLookupRpc { + @Type(() => Number) + @IsNumber({ allowNaN: false, allowInfinity: false }, { each: true }) + items!: Map + @IsString() orderId!: string +} + +@Message({ name: 'order-inventory-confirmation-rpc' }) +export class OrderInventoryConfirmationRpc { + @IsString() orderId!: string + @IsBoolean() confirmed!: string +} + +@Message({ name: 'order-finalize-request' }) +export class OrderFinalizeRequest { + @IsBoolean() finalize!: boolean +} + +@Message({ name: 'order-response' }) +export class OrderFinalizeResponse { + @IsBoolean() ack!: boolean +} diff --git a/test/e2e/asyncapi/inventory.spec.ts b/test/e2e/asyncapi/inventory.spec.ts new file mode 100644 index 0000000..be83b3c --- /dev/null +++ b/test/e2e/asyncapi/inventory.spec.ts @@ -0,0 +1,43 @@ +import { + collectProcessedMessages, + getProcessedMessageHandlerDecoratedMethods, +} from '../../../src' +import { AsyncapiSchema, DocumentBuilder } from '../../../src/lib/asyncapi' +import '../../setup' +import * as msgHandlers from './inventory.handlers' + +// biome-ignore lint/suspicious/noExportsInTest: +export const SCHEMA_POINTER_PREFIX = '#/components/schemas/' + +describe('Asyncapi generation with "inventory" app', () => { + test('generates expected asyncapi', async () => { + const handlers = [ + msgHandlers.HandlerInquiry, + msgHandlers.HandlerOrder, + msgHandlers.HandlerOrderSytem, + ].flatMap((handler) => getProcessedMessageHandlerDecoratedMethods(handler)) + + const asyncapiSchema = new AsyncapiSchema({ + SCHEMA_POINTER_PREFIX, + messages: collectProcessedMessages(), + messageHandlers: handlers, + }) + + const document = new DocumentBuilder() + .setTitle('test_invernory') + .setDescription('Test inverntory asnycapi') + .setVersion('1.0.0') + .setId('urn:id:global:test_invernory') + .build() + + const asyncapi = { + ...document, + components: { + schemas: asyncapiSchema.getSchemas(), + }, + channels: asyncapiSchema.getChannels(), + } + + expect(asyncapi).toMatchSnapshot() + }) +}) diff --git a/test/e2e/basic_publish_consume.spec.ts b/test/e2e/basic_publish_consume.spec.ts new file mode 100644 index 0000000..10259d5 --- /dev/null +++ b/test/e2e/basic_publish_consume.spec.ts @@ -0,0 +1,75 @@ +import { IsNumber, IsString } from 'class-validator' +import { ExchangeType, Message, MessageHandler, publish } from '../../src' +import { irisTesting } from '../setup' + +@Message({ name: 'foo' }) +class Foo { + @IsString() name!: string +} + +@Message({ name: 'bar', exchangeType: ExchangeType.direct }) +class Bar { + @IsNumber() age!: number +} + +@Message({ name: 'car' }) +class Car { + @IsString() car!: string + @IsNumber() carSize!: number +} + +class Handler { + @MessageHandler() + async handleFoo(_evt: Foo): Promise {} + + @MessageHandler() + async handleBar(_evt: Bar): Promise {} + + @MessageHandler() + async handleCar(_evt: Car): Promise {} +} + +describe('Publish and Consume events', () => { + let suite: irisTesting.integration.RegisterAndConnectReturnT + let handler: Handler + + beforeAll(async () => { + suite = await irisTesting.integration.registerAndConnect(Handler) + handler = suite.getHandlerFor(Handler)! + }) + + afterEach(async () => { + await suite.clearQueues() + }) + + afterAll(async () => { + await suite.deleteQueues() + }) + + test('Each handler should receive its own events', async () => { + const spyFoo = vi.spyOn(handler, 'handleFoo') + const spyBar = vi.spyOn(handler, 'handleBar') + const spyCar = vi.spyOn(handler, 'handleCar') + + await publish.getPublisher(Foo)({ name: 'foo_evt' }) + await publish.getPublisher(Foo)({ name: 'foo_evt2' }) + + await publish.getPublisher(Bar)({ age: -41 }) + + await publish.getPublisher(Car)({ car: 'car1', carSize: 1 }) + await publish.getPublisher(Car)({ car: 'car2', carSize: 2 }) + + await vi.waitFor(() => { + expect(spyFoo).toHaveBeenCalledTimes(2) + expect(spyFoo).toHaveBeenNthCalledWith(1, { name: 'foo_evt' }) + expect(spyFoo).toHaveBeenNthCalledWith(2, { name: 'foo_evt2' }) + + expect(spyBar).toHaveBeenCalledTimes(1) + expect(spyBar).toHaveBeenNthCalledWith(1, { age: -41 }) + + expect(spyCar).toHaveBeenCalledTimes(2) + expect(spyCar).toHaveBeenNthCalledWith(1, { car: 'car1', carSize: 1 }) + expect(spyCar).toHaveBeenNthCalledWith(2, { car: 'car2', carSize: 2 }) + }) + }) +}) diff --git a/test/e2e/connection.spec.ts b/test/e2e/connection.spec.ts new file mode 100644 index 0000000..e1bcb8a --- /dev/null +++ b/test/e2e/connection.spec.ts @@ -0,0 +1,60 @@ +import { ConnectionConfigI, connection } from '../../src' +import '../setup' + +const urlOrOpts = process.env.AMQP_URL + +describe('Connection', () => { + afterEach(async () => { + await connection.disconnect() + }) + + test('Call to disconnect will wait until connected if needed, before disconnecting', async () => { + const connCfg: ConnectionConfigI = { urlOrOpts } + + await Promise.all([ + connection.connect(connCfg), + connection.connect(connCfg), + connection.connect(connCfg), + connection.connect(connCfg), + connection.disconnect(), + connection.disconnect(), + connection.connect(connCfg), + connection.connect(connCfg), + ]) + + expect(connection.getConnection()).toBeUndefined() + + // should just ignore if already disconnected + await connection.disconnect() + }) + + test('Call to connect will wait until disconnected if needed, before connecting', async () => { + const connCfg: ConnectionConfigI = { urlOrOpts } + await connection.connect(connCfg) + + await Promise.all([ + connection.disconnect(), + connection.disconnect(), + connection.disconnect(), + connection.connect(connCfg), + connection.connect(connCfg), + connection.disconnect(), + connection.disconnect(), + ]) + + expect(connection.getConnection()).toBeDefined() + + // should just ignore if already connected + await connection.connect(connCfg) + }) + + test('assureChannel() will wait until connected if needed', async () => { + const connCfg: ConnectionConfigI = { urlOrOpts } + connection.connect(connCfg) + + expect(connection.getConnection()).toBeUndefined() + await expect(connection.assureChannel('foo')).resolves.toHaveProperty( + 'assertQueue', + ) + }) +}) diff --git a/test/e2e/error_handling.spec.ts b/test/e2e/error_handling.spec.ts new file mode 100644 index 0000000..17aab15 --- /dev/null +++ b/test/e2e/error_handling.spec.ts @@ -0,0 +1,213 @@ +import { IsEnum, IsString } from 'class-validator' +import { + constants, + AmqpMessage, + ExchangeType, + Message, + MessageHandler, + errors, + flags, + publish, +} from '../../src' +import { irisTesting } from '../setup' + +@Message({ + name: 'event-foo-direct', + exchangeType: ExchangeType.direct, + routingKey: 'foo', +}) +class Foo { + @IsString() name!: string +} + +@Message({ name: 'event-bar' }) +class Bar { + @IsString() name!: string +} + +/** + * Event to subscribe to `error` exchange in order to get reported errors + */ +@Message({ name: 'error', exchangeType: ExchangeType.topic }) +class ErrorEvt implements errors.ErrorMessageI { + @IsEnum(errors.ErrorTypeE) errorType!: errors.ErrorTypeE + @IsString() code!: string + @IsString() message!: string +} + +class Handler { + throwError?: Error + + throwThisError(error: Error): void { + this.throwError = error + } + + private doThrow(): void { + if (this.throwError !== undefined) { + throw this.throwError + } + } + + @MessageHandler() + handleFoo(_evt: Foo): void { + this.doThrow() + } + + @MessageHandler() + handleBar(_evt: Bar): void { + this.doThrow() + } + + @MessageHandler({ bindingKeys: '#' }) + handleError(_evt: ErrorEvt, _am: AmqpMessage): void {} +} + +describe('Error handling', () => { + let suite: irisTesting.integration.RegisterAndConnectReturnT + let handler: Handler + + beforeAll(async () => { + suite = await irisTesting.integration.registerAndConnect(Handler) + handler = suite.getHandlerFor(Handler)! + }) + + afterEach(async () => { + await suite.clearQueues() + }) + + afterAll(async () => { + await suite.deleteQueues() + }) + + test('Error of type `RejectMsgError` should be directly rejected and error sent to `error` exchange if notifyClient is set to true', async () => { + class NotFoundError extends errors.RejectMsgError { + errorType: errors.ErrorTypeE = errors.ErrorTypeE.NOT_FOUND + } + + const spyError = vi.spyOn(handler, 'handleError') + const spyFoo = vi.spyOn(handler, 'handleFoo') + + handler.throwThisError( + new errors.RejectMsgError('BECAUSE OF REASONS', true), + ) + + await publish.getPublisher(Foo)({ name: 'foo_evt' }, { routingKey: 'foo' }) + + await vi.waitFor(() => { + expect(spyFoo).toHaveBeenCalledTimes(1) + expect(spyFoo).toHaveBeenNthCalledWith(1, { name: 'foo_evt' }) + }) + + spyFoo.mockClear() + + handler.throwThisError(new NotFoundError('NOTHING HERE', true)) + + await publish.getPublisher(Foo)( + { name: 'foo_evt_2' }, + { routingKey: 'foo' }, + ) + + await vi.waitFor(() => { + expect(spyFoo).toHaveBeenCalledTimes(1) + expect(spyFoo).toHaveBeenNthCalledWith(1, { name: 'foo_evt_2' }) + }) + + await vi.waitFor(() => { + expect(spyError).toHaveBeenCalledTimes(2) + expect(spyError).toHaveBeenNthCalledWith( + 1, + { + errorType: errors.ErrorTypeE.BAD_REQUEST, + code: 'RejectMsgError', + message: 'BECAUSE OF REASONS', + }, + expect.anything(), + ) + expect(spyError).toHaveBeenNthCalledWith( + 2, + { + errorType: errors.ErrorTypeE.NOT_FOUND, + code: 'NotFoundError', + message: 'NOTHING HERE', + }, + expect.anything(), + ) + + expect(spyError.mock.calls.at(0)![1]).toEqual( + expect.objectContaining({ + fields: expect.objectContaining({ + routingKey: 'event-foo-direct.error', + }), + properties: expect.objectContaining({ + headers: expect.objectContaining({ + 'x-event-type': 'error', + }), + }), + }), + ) + + expect(spyError.mock.calls.at(1)![1]).toEqual( + expect.objectContaining({ + fields: expect.objectContaining({ + routingKey: 'event-foo-direct.error', + }), + properties: expect.objectContaining({ + headers: expect.objectContaining({ + 'x-event-type': 'error', + }), + }), + }), + ) + }) + }) + + test('Message should be directly rejected if structure does not correspond to specified message class', async () => { + const spyError = vi.spyOn(handler, 'handleError') + const spyFoo = vi.spyOn(handler, 'handleFoo') + + const currentDisableProduceValidationFlag = + flags.DISABLE_MESSAGE_PRODUCE_VALIDATION + flags.DISABLE_MESSAGE_PRODUCE_VALIDATION = true + const invalidFooEvent = ({ name: 1234 }) + await publish.getPublisher(Foo)(invalidFooEvent, { + routingKey: 'foo', + amqpPublishOpts: { + headers: { + // error is auto sent to `error` exchange if session is found + [constants.MESSAGE_HEADERS.MESSAGE.SESSION_ID]: 'foo-id', + }, + }, + }) + flags.DISABLE_MESSAGE_PRODUCE_VALIDATION = + currentDisableProduceValidationFlag + + await vi.waitFor(() => { + expect(spyFoo).toHaveBeenCalledTimes(0) + expect(spyError).toHaveBeenCalledTimes(1) + + expect(spyError).toHaveBeenNthCalledWith( + 1, + { + errorType: errors.ErrorTypeE.BAD_REQUEST, + code: 'InvalidObjectConverionError', + message: + '[{"target":{"name":1234},"value":1234,"property":"name","children":[],"constraints":{"isString":"name must be a string"}}]', + }, + expect.anything(), + ) + + expect(spyError.mock.calls.at(0)![1]).toEqual( + expect.objectContaining({ + fields: expect.objectContaining({ + routingKey: 'event-foo-direct.error', + }), + properties: expect.objectContaining({ + headers: expect.objectContaining({ + 'x-event-type': 'error', + }), + }), + }), + ) + }) + }) +}) diff --git a/test/e2e/event_enrichment.spec.ts b/test/e2e/event_enrichment.spec.ts new file mode 100644 index 0000000..1032f0d --- /dev/null +++ b/test/e2e/event_enrichment.spec.ts @@ -0,0 +1,153 @@ +import { Type } from 'class-transformer' +import { + IsDate, + IsInstance, + IsNumber, + IsOptional, + IsString, +} from 'class-validator' +import { AmqpMessage, Message, MessageHandler, publish } from '../../src' +import { irisTesting } from '../setup' + +@Message({ name: 'event-one' }) +class EvtBase { + @IsNumber() @IsOptional() idx?: number + @IsString({ each: true }) callStack!: string[] +} + +@Message({ name: 'event-two' }) +class EvtTwo extends EvtBase {} + +@Message({ name: 'event-three' }) +class EvtThree extends EvtBase {} + +@Message({ name: 'collected-evts' }, { classTransform: {} }) +class EvtCollected { + @IsInstance(EvtBase) @Type(() => EvtBase) collectedEvents!: EvtBase +} + +@Message({ name: 'end-result' }) +class EvtResult { + @IsInstance(EvtCollected) @Type(() => EvtCollected) collected!: EvtCollected + @IsDate() @Type(() => Date) timestamp!: Date +} + +class Handler { + static timestamp: Date = new Date() + static statiCallstackTag = 'static-access-works' + + @MessageHandler({}, EvtTwo) + static handleOne(evt: EvtBase): EvtTwo { + return Handler.enrich(evt, Handler.statiCallstackTag) + } + + @MessageHandler() + handleTwo(evt: EvtTwo, msg: AmqpMessage): void { + setTimeout(() => { + publish + .publishReply(msg, EvtThree, this.enrich(evt, 'two'), { + amqpPublishOpts: { headers: { 'handler-two': 'additional header' } }, + }) + .catch((e) => { + throw e + }) + }, 1000) + } + + @MessageHandler({}, EvtCollected) + handleThree(evt: EvtThree): EvtCollected { + return { + collectedEvents: this.enrich(evt, 'three'), + } + } + + @MessageHandler({}, EvtResult) + static async handleCollected(evt: EvtCollected): Promise { + return new Promise((resolve) => { + setTimeout(() => { + evt.collectedEvents.callStack.push('final') + resolve({ + collected: evt, + // static method should also be properly bound to class + timestamp: Handler.timestamp, + }) + }, 340) + }) + } + + @MessageHandler() + handleEvtResult(_evt: EvtResult, _amqp: AmqpMessage) {} + + private enrich(evt: EvtBase, tag: string): EvtBase { + return Handler.enrich(evt, tag) + } + private static enrich(evt: EvtBase, tag: string): EvtBase { + return { + ...evt, + callStack: [...evt.callStack, tag], + idx: (evt.idx ?? 0) + 1, + } + } +} + +describe('Event Enrichment using reply mechanism', () => { + let suite: irisTesting.integration.RegisterAndConnectReturnT + let handler: Handler + + beforeAll(async () => { + suite = await irisTesting.integration.registerAndConnect(Handler) + handler = suite.getHandlerFor(Handler)! + }) + + afterEach(async () => { + await suite.clearQueues() + }) + + afterAll(async () => { + await suite.deleteQueues() + }) + + test('Event propagation through multiple handlers', async () => { + const spyResult = vi.spyOn(handler, 'handleEvtResult') + + await publish.getPublisher(EvtBase)( + { callStack: ['snowball'] }, + { + amqpPublishOpts: { + headers: { + 'my-foo-header': 'should be propagated', + }, + }, + }, + ) + + await vi.waitFor(() => { + expect(spyResult).toHaveBeenCalledOnce() + expect( + JSON.parse(JSON.stringify(spyResult.mock.calls[0][0])), + ).toMatchObject({ + collected: { + collectedEvents: { + callStack: [ + 'snowball', + Handler.statiCallstackTag, + 'two', + 'three', + 'final', + ], + idx: 3, + }, + }, + timestamp: Handler.timestamp.toISOString(), + }) + expect(spyResult.mock.calls[0][1]).toMatchObject({ + properties: { + headers: { + 'my-foo-header': 'should be propagated', + 'handler-two': 'additional header', + }, + }, + }) + }, 3000) + }) +}) diff --git a/test/e2e/external_scopes.spec.ts b/test/e2e/external_scopes.spec.ts new file mode 100644 index 0000000..98a9d21 --- /dev/null +++ b/test/e2e/external_scopes.spec.ts @@ -0,0 +1,244 @@ +import { Type } from 'class-transformer' +import { IsEnum, IsInstance, IsString } from 'class-validator' +import _ from 'lodash' +import { + AmqpMessage, + ExchangeType, + Message, + MessageHandler, + Scope, + publish, +} from '../../src' +import { irisTesting } from '../setup' + +@Message({ + name: 'evt-frontend', + scope: Scope.FRONTEND, + exchangeType: ExchangeType.direct, +}) +class EvtFrontend { + @IsString({ each: true }) messages!: string[] + @IsEnum(Scope) targetExchange!: Scope +} + +@Message({ + name: 'evt-frontend-two', + scope: Scope.FRONTEND, + exchangeType: ExchangeType.direct, +}) +class EvtFrontendTwo extends EvtFrontend {} + +@Message({ + name: 'evt-frontend-three', + scope: Scope.FRONTEND, + exchangeType: ExchangeType.direct, +}) +class EvtFrontendThree extends EvtFrontend {} + +@Message({ + name: 'from-user-scope', + scope: Scope.USER, + exchangeType: ExchangeType.direct, +}) +class EvtUser { + @IsInstance(EvtFrontend) @Type(() => EvtFrontend) originalEvt!: EvtFrontend + @IsString() routingKey!: string +} + +@Message({ + name: 'from-session-scope', + scope: Scope.SESSION, + exchangeType: ExchangeType.direct, +}) +class EvtSession extends EvtUser {} + +@Message({ + name: 'from-broadcast-scope', + scope: Scope.BROADCAST, + exchangeType: ExchangeType.direct, +}) +class EvtBroadcast extends EvtUser {} + +class Handler { + @MessageHandler({}, EvtFrontend) + async handleFrontend(evt: EvtFrontend, msg: AmqpMessage): Promise { + await this.publishForScope(evt, msg.fields.routingKey) + } + + @MessageHandler({}, EvtFrontendTwo) + async handleFrontendTwo( + evt: EvtFrontendTwo, + msg: AmqpMessage, + ): Promise { + await this.publishForScope(evt, msg.fields.routingKey) + } + + @MessageHandler({}, EvtFrontendThree) + async handleFrontendThree( + evt: EvtFrontendThree, + msg: AmqpMessage, + ): Promise { + await this.publishForScope(evt, msg.fields.routingKey) + } + + private async publishForScope( + evt: EvtFrontend, + routingKey: string, + ): Promise { + switch (evt.targetExchange) { + case Scope.USER: + await publish.getPublisher(EvtUser)( + { originalEvt: evt, routingKey }, + { userId: 'user-id' }, + ) + break + case Scope.SESSION: + await publish.getPublisher(EvtSession)( + { originalEvt: evt, routingKey }, + { amqpPublishOpts: { headers: { 'x-session-id': 'session-id' } } }, + ) + break + case Scope.BROADCAST: + await publish.getPublisher(EvtBroadcast)({ + originalEvt: evt, + routingKey, + }) + break + default: + throw new Error('invalid test') + } + } +} + +describe('External scopes (frontend/user/session). Testing with 3 frontend events to test internal frontend "router".', () => { + let suite: irisTesting.integration.RegisterAndConnectReturnT + let handler: Handler + + const frontEvtClasses: (typeof EvtFrontend)[] = [ + EvtFrontend, + EvtFrontendTwo, + EvtFrontendThree, + ] + + beforeAll(async () => { + suite = await irisTesting.integration.registerAndConnect(Handler) + handler = suite.getHandlerFor(Handler)! + }) + + afterAll(async () => { + await suite.deleteQueues() + }) + + for (const evtClass of frontEvtClasses) { + // specified in @Message to be same as class name + const expectedRoutingKey = _.kebabCase(evtClass.name) + + test(`Consume FRONTEND and publish to USER using ${evtClass.name}`, async () => { + const spy = vi.fn() + const unsubscribe = await irisTesting.utilities.subscribe(EvtUser, spy) + + await irisTesting.utilities.publishToFrontend(evtClass, { + messages: ['hello'], + targetExchange: Scope.USER, + }) + + await vi.waitFor(() => { + expect(spy).toHaveBeenCalledOnce() + expect(spy).toBeCalledWith( + { + originalEvt: { messages: ['hello'], targetExchange: Scope.USER }, + routingKey: expectedRoutingKey, + }, + expect.anything(), + ) + }) + + await unsubscribe() + }) + + test(`Consume FRONTEND and publish to SESSION using ${evtClass.name}`, async () => { + const spy = vi.fn() + const unsubscribe = await irisTesting.utilities.subscribe(EvtSession, spy) + + await irisTesting.utilities.publishToFrontend(evtClass, { + messages: ['hello'], + targetExchange: Scope.SESSION, + }) + + await vi.waitFor(() => { + expect(spy).toHaveBeenCalledOnce() + expect(spy).toBeCalledWith( + { + originalEvt: { messages: ['hello'], targetExchange: Scope.SESSION }, + routingKey: expectedRoutingKey, + }, + expect.anything(), + ) + }) + + await unsubscribe() + }) + + test(`Consume FRONTEND and publish to BROADCAST using ${evtClass.name}`, async () => { + const spy = vi.fn() + const unsubscribe = await irisTesting.utilities.subscribe( + EvtBroadcast, + spy, + ) + + await irisTesting.utilities.publishToFrontend(evtClass, { + messages: ['hello'], + targetExchange: Scope.BROADCAST, + }) + + await vi.waitFor(() => { + expect(spy).toHaveBeenCalledOnce() + expect(spy).toBeCalledWith( + { + originalEvt: { + messages: ['hello'], + targetExchange: Scope.BROADCAST, + }, + routingKey: expectedRoutingKey, + }, + expect.anything(), + ) + }) + + await unsubscribe() + }) + } + + test('Publish to USER scope without x-user-id should throw', async () => { + await expect( + publish.getPublisher(EvtUser)({ + originalEvt: { targetExchange: Scope.USER, messages: [] }, + routingKey: 'rk', + }), + ).rejects.toEqual( + new Error('ERR_IRIS_PUBLISH_TO_USER_SCOPE_WITHOUT_USER_ID'), + ) + }) + + test('Publish to SESSION scope without x-session-id should throw', async () => { + await expect( + publish.getPublisher(EvtSession)({ + originalEvt: { targetExchange: Scope.SESSION, messages: [] }, + routingKey: 'rk', + }), + ).rejects.toEqual( + new Error('ERR_IRIS_PUBLISH_TO_SESSION_SCOPE_WITHOUT_SESSION_ID'), + ) + }) + + test('Publish to FRONTENT scope is not supported', async () => { + await expect( + publish.getPublisher(EvtFrontend)({ + messages: [], + targetExchange: Scope.FRONTEND, + }), + ).rejects.toEqual( + new Error('ERR_IRIS_PUBLISH_TO_FRONTENT_SCOPE_NOT_SUPPORTED'), + ) + }) +}) diff --git a/test/e2e/mdc.spec.ts b/test/e2e/mdc.spec.ts new file mode 100644 index 0000000..c2327dc --- /dev/null +++ b/test/e2e/mdc.spec.ts @@ -0,0 +1,102 @@ +import { randomUUID } from 'node:crypto' +import { IsString } from 'class-validator' +import { + Message, + MessageHandler, + getProcessedMessageDecoratedClass, + mdc, + publish, +} from '../../src' +import { irisTesting } from '../setup' + +@Message({ name: `foo-${randomUUID()}` }) +class Foo { + @IsString() name!: string +} + +class Handler { + @MessageHandler() + async handleFoo(_evt: Foo, _mdc: mdc.MDC): Promise {} +} + +describe('MDC', () => { + let suite: irisTesting.integration.RegisterAndConnectReturnT + let handler: Handler + + beforeAll(async () => { + suite = await irisTesting.integration.registerAndConnect(Handler) + handler = suite.getHandlerFor(Handler)! + }) + + afterEach(async () => { + await suite.clearQueues() + }) + + afterAll(async () => { + await suite.deleteQueues() + }) + + test('Expect full MDC when all mdc related headers are present', async () => { + const mdc: mdc.MdcI = { + sessionId: randomUUID(), + userId: randomUUID(), + clientTraceId: randomUUID(), + correlationId: randomUUID(), + eventType: randomUUID(), + clientVersion: randomUUID(), + } + + const spyFoo = vi.spyOn(handler, 'handleFoo') + + await publish.getPublisher(Foo)( + { name: 'foo_evt' }, + { + amqpPublishOpts: { + headers: { + 'x-session-id': mdc.sessionId, + 'x-user-id': mdc.userId, + 'x-client-trace-id': mdc.clientTraceId, + 'x-correlation-id': mdc.correlationId, + 'x-event-type': mdc.eventType, + 'x-client-version': mdc.clientVersion, + }, + }, + }, + ) + + await vi.waitFor(() => { + expect(spyFoo).toHaveBeenCalledTimes(1) + expect(spyFoo).toHaveBeenNthCalledWith(1, { name: 'foo_evt' }, mdc) + }) + }) + + test('Expect partial MDC when some mdc related headers are present', async () => { + const mdc: mdc.MdcI = { + sessionId: randomUUID(), + userId: randomUUID(), + clientVersion: randomUUID(), + eventType: + getProcessedMessageDecoratedClass(Foo).processedConfig!.exchangeName, + } + + const spyFoo = vi.spyOn(handler, 'handleFoo') + + await publish.getPublisher(Foo)( + { name: 'foo_evt' }, + { + amqpPublishOpts: { + headers: { + 'x-session-id': mdc.sessionId, + 'x-user-id': mdc.userId, + 'x-client-version': mdc.clientVersion, + }, + }, + }, + ) + + await vi.waitFor(() => { + expect(spyFoo).toHaveBeenCalledTimes(1) + expect(spyFoo).toHaveBeenNthCalledWith(1, { name: 'foo_evt' }, mdc) + }) + }) +}) diff --git a/test/e2e/prefetch.spec.ts b/test/e2e/prefetch.spec.ts new file mode 100644 index 0000000..51f367e --- /dev/null +++ b/test/e2e/prefetch.spec.ts @@ -0,0 +1,118 @@ +import { IsString } from 'class-validator' +import { Message, MessageHandler, publish } from '../../src' +import { Pausable } from '../helpers' +import { irisTesting } from '../setup' + +@Message({ name: 'ping-1' }) +class Ping1 { + @IsString() msg!: string +} +@Message({ name: 'ping-2' }) +class Ping2 { + @IsString() msg!: string +} +@Message({ name: 'ping-4' }) +class Ping4 { + @IsString() msg!: string +} + +class Handler extends Pausable { + private receivedMessages: string[] = [] + + constructor() { + super() + this.pause() + } + + public getReceivedMessages(): string[] { + return this.receivedMessages + } + + @MessageHandler({ prefetch: 1 }) + async ping1(evt: Ping1): Promise { + this.receivedMessages.push(evt.msg) + await this.paused + } + + @MessageHandler({ prefetch: 2 }) + async ping2(evt: Ping2): Promise { + this.receivedMessages.push(evt.msg) + await this.paused + } + + @MessageHandler({ prefetch: 4 }) + async ping4(evt: Ping4): Promise { + this.receivedMessages.push(evt.msg) + await this.paused + } +} + +describe('Prefetch', () => { + let suite: irisTesting.integration.RegisterAndConnectReturnT + let handler: Handler + + beforeAll(async () => { + suite = await irisTesting.integration.registerAndConnect(Handler) + handler = suite.getHandlerFor(Handler)! + }) + + afterEach(async () => { + await suite.clearQueues() + }) + + afterAll(async () => { + await suite.deleteQueues() + }) + + test('Prefetch should only allow specified number of not handled messages being handled at same time', async () => { + const pub1 = publish.getPublisher(Ping1) + const pub2 = publish.getPublisher(Ping2) + const pub4 = publish.getPublisher(Ping4) + + const messages = new Array(10).fill('msg') + const msgs1 = messages.map((msg) => `${msg}_1`) + const msgs2 = messages.map((msg) => `${msg}_2`) + const msgs4 = messages.map((msg) => `${msg}_4`) + + const expectedWhenBlocking = [ + ...msgs1.slice(0, 1), + ...msgs2.slice(0, 2), + ...msgs4.slice(0, 4), + ] + const expectedWhenBlockingSecondTime = [ + ...msgs1.slice(0, 2), + ...msgs2.slice(0, 4), + ...msgs4.slice(0, 8), + ] + const expectedWhenUnblocked = [...msgs1, ...msgs2, ...msgs4] + + await Promise.all([ + ...msgs1.map(async (msg) => pub1({ msg })), + ...msgs2.map(async (msg) => pub2({ msg })), + ...msgs4.map(async (msg) => pub4({ msg })), + ]) + + await vi.waitFor(() => { + expect(handler.getReceivedMessages().sort()).toEqual( + expectedWhenBlocking.sort(), + ) + }) + + handler.resume() + handler.pause() + + await vi.waitFor(() => { + expect(handler.getReceivedMessages().sort()).toEqual( + expectedWhenBlockingSecondTime.sort(), + ) + }) + + handler.resume() + + await vi.waitFor(() => { + expect(handler.getReceivedMessages().sort()).toEqual( + expectedWhenUnblocked.sort(), + ) + }) + }) +}) diff --git a/test/e2e/publish_to_user.spec.ts b/test/e2e/publish_to_user.spec.ts new file mode 100644 index 0000000..e5a6e9a --- /dev/null +++ b/test/e2e/publish_to_user.spec.ts @@ -0,0 +1,139 @@ +import { IsString } from 'class-validator' +import { + constants, + AmqpMessage, + Message, + MessageHandler, + Scope, + publish, +} from '../../src' +import { irisTesting } from '../setup' + +@Message({ + name: constants.MANAGED_EXCHANGES.USER.EXCHANGE, + exchangeType: constants.MANAGED_EXCHANGES.USER.EXCHANGE_TYPE, + routingKey: 'user', +}) +class EvtUser { + @IsString() msg!: string +} + +@Message({ + name: 'session-msg', + scope: Scope.SESSION, +}) +class EvtSession { + @IsString() msg!: string +} + +@Message({ name: 'internal-msg' }) +class EvtInternal { + @IsString() msg!: string +} + +class Handler { + @MessageHandler({ bindingKeys: '#' }) + async publishInternalToUser(evt: EvtUser, amqpMsg: AmqpMessage) { + if (evt.msg === 'do-publish-internal-to-user') { + await publish.getUserPublisher(EvtInternal)({ msg: 'hello' }, amqpMsg) + } + } +} + +describe('Publishing non Scope.USER messages to user via publish.[publishToUser/getUserPublisher]', () => { + let suite: irisTesting.integration.RegisterAndConnectReturnT + let handler: Handler + + beforeAll(async () => { + suite = await irisTesting.integration.registerAndConnect(Handler) + handler = suite.getHandlerFor(Handler)! + }) + + afterAll(async () => { + await suite.deleteQueues() + }) + + test('Sending SESSION event to USER should work', async () => { + const spyHandler = vi.spyOn(handler, 'publishInternalToUser') + const headers = { foo: `bar_${Date.now()}` } + + await publish.publishToUser( + EvtSession, + { msg: 'do-publish-internal-to-user' }, + 'user-id', + { amqpPublishOpts: { headers } }, + ) + + await vi.waitFor(() => { + expect(spyHandler).toHaveBeenCalledTimes(2) + expect(spyHandler).toHaveBeenNthCalledWith( + 1, + { msg: 'do-publish-internal-to-user' }, + expect.anything(), + ) + expect(spyHandler).toHaveBeenNthCalledWith( + 2, + { msg: 'hello' }, + expect.objectContaining({ + fields: expect.any(Object), + content: expect.any(Buffer), + properties: expect.objectContaining({ + headers: expect.objectContaining(headers), + }), + }), + ) + }) + }) + + test('Sending INTERNAL event to USER should work', async () => { + const spyHandler = vi.spyOn(handler, 'publishInternalToUser') + const headers = { foo: `bar_${Date.now()}` } + + await publish.publishToUser( + EvtInternal, + { msg: 'do-publish-internal-to-user' }, + 'user-id', + { amqpPublishOpts: { headers } }, + ) + + await vi.waitFor(() => { + expect(spyHandler).toHaveBeenCalledTimes(2) + expect(spyHandler).toHaveBeenNthCalledWith( + 1, + { msg: 'do-publish-internal-to-user' }, + expect.anything(), + ) + expect(spyHandler).toHaveBeenNthCalledWith( + 2, + { msg: 'hello' }, + expect.objectContaining({ + fields: expect.any(Object), + content: expect.any(Buffer), + properties: expect.objectContaining({ + headers: expect.objectContaining(headers), + }), + }), + ) + }) + }) + + test('Should throw if user-id is missing', async () => { + await expect( + publish.publishToUser( + EvtInternal, + { msg: 'do-publish-internal-to-user' }, + // @ts-ignore + {}, + ), + ).rejects.toThrow('ERR_IRIS_PUBLISHER_USER_ID_NOT_RESOLVED') + + await expect( + publish.publishToUser( + EvtInternal, + { msg: 'do-publish-internal-to-user' }, + // @ts-ignore + new EvtInternal(), + ), + ).rejects.toThrow('ERR_IRIS_PUBLISHER_USER_ID_NOT_RESOLVED') + }) +}) diff --git a/test/e2e/resilience.spec.ts b/test/e2e/resilience.spec.ts new file mode 100644 index 0000000..362d315 --- /dev/null +++ b/test/e2e/resilience.spec.ts @@ -0,0 +1,284 @@ +import { randomUUID } from 'node:crypto' +import { IsString } from 'class-validator' +import _ from 'lodash' +import { MockInstance } from 'vitest' +import * as rabbit from '../rabbit' +import { irisTesting } from '../setup' + +import { + constants, + AmqpMessage, + Message, + MessageHandler, + connection, + publish, +} from '../../src' + +@Message({ name: 'ping' }) +class Ping { + @IsString() msg!: string +} + +@Message({ name: 'pong' }) +class Pong { + @IsString() msg!: string +} + +class ResilienceHandler { + @MessageHandler({}, Pong) + ping(evt: Ping): Pong { + return { msg: evt.msg } + } + + @MessageHandler() + pong(_evt: Pong, _amqpMsg: AmqpMessage) {} +} + +// rabbit's admin APIs do not return actual state immediately +const RABBIT_ADMIN_REFRESH_DELAY = 4000 + +describe.runIf(process.env.TESTS_SKIP_RESILIENCE !== '1')('Resilience', () => { + let suite: irisTesting.integration.RegisterAndConnectReturnT + let handler: ResilienceHandler + + let spyPong: MockInstance + + const reinitializationDelay = constants.getReinitializationDelay() + + beforeAll(async () => { + suite = await irisTesting.integration.registerAndConnect(ResilienceHandler) + handler = suite.getHandlerFor(ResilienceHandler)! + spyPong = vi.spyOn(handler, 'pong') + + constants.setReinitializationDelay(100) + }) + + async function waitForReinitialization(): Promise { + const reinitDelay = constants.getReinitializationDelay() + // give it some time to re-assert queues, re-open channesl etc + const waitForAssertions = 400 + + return new Promise((resolve) => + setTimeout(resolve, reinitDelay + waitForAssertions), + ) + } + + async function testConnectionViaMessage(msg: string): Promise { + const uniqueMsg = `${msg}_${randomUUID()}` + await publish.getPublisher(Ping)({ msg: uniqueMsg }) + await vi.waitFor(() => { + expect(spyPong).toHaveBeenCalledWith( + { msg: uniqueMsg }, + expect.anything(), + ) + }, 20000) + + expect(spyPong).toHaveBeenCalledOnce() + } + + function expectConnected() { + expect(connection.isDisconnected()).toEqual(false) + expect(connection.isReconnecting()).toEqual(false) + } + + afterAll(async () => { + constants.setReinitializationDelay(reinitializationDelay) + await rabbit.adminDeleteAllTestUsers() + if (connection.isDisconnected()) { + await irisTesting.utilities.connect() + } + suite.deleteQueues() + }) + + describe('Reconnect', () => { + test('Test setup', async () => { + const msg = 'foo' + await testConnectionViaMessage(msg) + }) + + test('Service should recover when channels are closed', async () => { + const msg = 'foo_after_channel_close' + const channel = await irisTesting.utilities.getChannelForMessage(Ping) + await channel.close() + await waitForReinitialization() + + await testConnectionViaMessage(msg) + expectConnected() + }) + + test('Service should recover when queues are deleted', async () => { + const msg = 'foo_after_queue_delete' + await suite.deleteQueues(true) + + await waitForReinitialization() + + await testConnectionViaMessage(msg) + expectConnected() + }) + + test("Service should recover when queue and it's exchange are deleted", async () => { + const msg = 'foo_after_queue_delete' + await suite.deleteQueues(true) + await suite.deleteExchange(Ping) + + await waitForReinitialization() + + await testConnectionViaMessage(msg) + expectConnected() + }) + + test('Service should recover when connection is closed', async () => { + const msg = 'foo_after_connection_close' + + await connection.getConnection()!.close() + await waitForReinitialization() + + await testConnectionViaMessage(msg) + expectConnected() + }) + + test('Service should recover when connection is closed with error', async () => { + const msg = 'foo_after_connection_close_with_error' + expectConnected() + + await vi.waitFor(async () => { + const connectionNames = await rabbit.adminGetConnectionNames() + expect(connectionNames.length).toBeGreaterThan(0) + }, 10000) // it can take long time for rabbit to return something + + // kill connection via admin api which results in + // 'error' being present on 'close' event + await rabbit.adminCloseAllConnections() + await waitForReinitialization() + + await testConnectionViaMessage(msg) + expectConnected() + }, 10000) + }) + + // test this in a loop, making sure that lib can be + // reused after disconnected + test.each([1, 2, 3])( + 'Service should NOT recover when connection is closed intentionally, pass %i', + async (idx) => { + await irisTesting.integration.registerAndConnect(ResilienceHandler) + await connection.disconnect() + await waitForReinitialization() + await expect( + publish.getPublisher(Ping)({ msg: `will fail ${idx}` }), + ).rejects.toThrow('ERR_IRIS_CONNECTION_NOT_ESTABLISHED') + expect(connection.isDisconnected()).toEqual(true) + expect(connection.isReconnecting()).toEqual(false) + }, + ) + + describe('Reconnect retrying', () => { + const reconnectInterval = 300 + const reconnectTries = 4 + let testCredentials: { username: string; password: string } + + beforeEach(async () => { + const amqpUrl = new URL(rabbit.getAmqpUrl()) + testCredentials = await rabbit.adminUpsertUser() + await connection.disconnect() + expect(connection.isReconnecting()).toEqual(false) + + suite = await irisTesting.integration.registerAndConnect( + ResilienceHandler, + { + // can not use spread with URL class + urlOrOpts: _.merge({}, amqpUrl, { + username: testCredentials.username, + password: testCredentials.password, + protocol: amqpUrl.protocol.replace(':', ''), + }), + reconnectInterval, + reconnectTries, + reconnectFactor: 0, // less math timing the tests + }, + ) + + handler = suite.getHandlerFor(ResilienceHandler)! + spyPong = vi.spyOn(handler, 'pong') + + expectConnected() + }) + + test('Service should recover after a while when connection is closed with error', async () => { + let msg = `foo_after_connection_recovered_via_reconnect_retries_${randomUUID()}` + await vi.waitFor(async () => { + const connectionNames = await rabbit.adminGetConnectionNames() + expect(connectionNames.length).toBeGreaterThan(0) + }, 10000) + + await testConnectionViaMessage(`${msg}_initial`) + spyPong.mockReset() + + // change user's password and kill the connection + // so that recoonecting will fail + await rabbit.adminUpsertUser(testCredentials.username, randomUUID()) + await rabbit.adminCloseAllConnections() + + // Publish now but not wait. + // Requesting a channel during reconnect flow should + // wait until conneciton is re-established and then + // work correctly. + msg = `foo_after_connection_recovered_via_reconnect_retries_${randomUUID()}` + const pendingPublish = (async () => { + await publish.getPublisher(Ping)({ msg }) + })() + + const approxMidReconnectTry = reconnectInterval * 2 + 5 + + setTimeout(() => { + // change password back to the one used by current connection + rabbit.adminUpsertUser( + testCredentials.username, + testCredentials.password, + ) + }, approxMidReconnectTry) + + await vi.waitFor( + async () => { + const connectionNames = await rabbit.adminGetConnectionNames() + expect(connectionNames.length).toBeGreaterThan(0) + expectConnected() + }, + { + timeout: + reconnectInterval * reconnectTries + RABBIT_ADMIN_REFRESH_DELAY, + interval: reconnectInterval / 2, + }, + ) + + await pendingPublish + await vi.waitFor(() => { + expect(spyPong).toHaveBeenCalledWith({ msg }, expect.anything()) + }, 20000) + }, 20000) + + test('Service should NOT recover after maximum reconnect tries', async () => { + const msg = `foo_after_connection_should_not_recover_${randomUUID()}` + await vi.waitFor(async () => { + const connectionNames = await rabbit.adminGetConnectionNames() + expect(connectionNames.length).toBeGreaterThan(0) + }, 10000) + + await testConnectionViaMessage(`${msg}_initial`) + + // change user's password and kill the connection + // so that recoonecting will fail + await rabbit.adminUpsertUser(testCredentials.username, randomUUID()) + await rabbit.adminCloseAllConnections() + + await vi.waitFor(async () => { + const connectionNames = await rabbit.adminGetConnectionNames() + expect(connectionNames.length).toBe(0) + }) + + await expect(publish.getPublisher(Ping)({ msg })).rejects.toThrow( + 'ERR_IRIS_CONNECTION_NOT_ESTABLISHED', + ) + }, 10000) + }) +}) diff --git a/test/e2e/retry.spec.ts b/test/e2e/retry.spec.ts new file mode 100644 index 0000000..6dd2758 --- /dev/null +++ b/test/e2e/retry.spec.ts @@ -0,0 +1,295 @@ +import { IsString } from 'class-validator' +import { + constants, + AmqpMessage, + ExchangeType, + Message, + MessageHandler, + errors, + helper, + publish, +} from '../../src' +import { irisTesting } from '../setup' + +@Message({ name: 'event-foo-dlq', maxRetry: 10 }) +class FooDlqMsq { + @IsString() name!: string +} + +@Message({ + name: 'event-bar-dlq-custom-deadletter', + deadLetter: 'bar-died', + exchangeType: ExchangeType.direct, + routingKey: 'bar-custom-dlq', +}) +class BarCustomDLQMsg { + @IsString() name!: string +} + +/** + * This exchange is otherwise managed by manager service + */ +@Message( + { name: 'retry', exchangeType: ExchangeType.direct }, + { validate: false }, +) +class Retry {} + +@Message( + { + name: constants.MANAGED_EXCHANGES.DEAD_LETTER.EXCHANGE, + exchangeType: constants.MANAGED_EXCHANGES.DEAD_LETTER.EXCHANGE_TYPE, + routingKey: '#', + }, + { validate: false }, +) +class DefaultDLQ {} + +@Message( + { + name: 'dead.bar-died', + exchangeType: constants.MANAGED_EXCHANGES.DEAD_LETTER.EXCHANGE_TYPE, + routingKey: '#', + }, + { validate: false }, +) +class CustomDlq {} + +class Handler { + throwError?: Error + + @MessageHandler() + handleFooDqlMsg(_evt: FooDlqMsq): void { + this.throwIfNeeded() + } + + @MessageHandler() + handleBarCustomDlqMsg(_evt: BarCustomDLQMsg): void { + this.throwIfNeeded() + } + + @MessageHandler({ bindingKeys: 'retry' }) + listenOnRetry(_evt: Retry, _am: AmqpMessage): void {} + + @MessageHandler() + listenOnDefaultDlq(_evt: DefaultDLQ, _am: AmqpMessage): void {} + + @MessageHandler() + listenOnCustomDlq(_evt: CustomDlq, _am: AmqpMessage): void {} + + throwThisError(error: Error): void { + this.throwError = error + } + + private throwIfNeeded(): void { + if (this.throwError !== undefined) { + throw this.throwError + } + } +} + +describe('Retry / DLQ', () => { + let suite: irisTesting.integration.RegisterAndConnectReturnT + let handler: Handler + + beforeAll(async () => { + suite = await irisTesting.integration.registerAndConnect(Handler) + handler = suite.getHandlerFor(Handler)! + }) + + afterEach(async () => { + await suite.clearQueues() + }) + + afterAll(async () => { + await suite.deleteQueues() + }) + + test('Message should be sent to retry queue when not rejected', async () => { + class SomeError extends errors.MsgError {} + class SomeOtherError extends errors.MsgError { + errorType = errors.ErrorTypeE.BAD_REQUEST + } + const spyRetry = vi.spyOn(handler, 'listenOnRetry') + const spyFooDlq = vi.spyOn(handler, 'handleFooDqlMsg') + const spyBarDlqCustom = vi.spyOn(handler, 'handleBarCustomDlqMsg') + + const fooEvtName = `foo_evt_${Date.now()}` + + handler.throwThisError( + new SomeError('I am going to retry..').setNotifyClient(true), + ) + await publish.getPublisher(FooDlqMsq)({ name: fooEvtName }) + + await vi.waitFor(() => { + expect(spyFooDlq).toHaveBeenCalledTimes(1) + expect(spyRetry).toHaveBeenCalledTimes(1) + expect(spyRetry).toHaveBeenNthCalledWith( + 1, + { name: fooEvtName }, + expect.anything(), + ) + + expect(spyRetry.mock.calls.at(0)![1]).toEqual( + expect.objectContaining({ + fields: expect.objectContaining({ + routingKey: 'retry', + }), + + properties: expect.objectContaining({ + headers: expect.objectContaining({ + 'x-original-exchange': 'event-foo-dlq', + 'x-original-routing-key': 'event-foo-dlq', + 'x-max-retries': 10, + 'x-event-type': 'event-foo-dlq', + 'x-dead-letter-exchange': 'dead.dead-letter', + 'x-dead-letter-routing-key': `dead.${helper.getServiceName()}.event-foo-dlq`, + 'x-notify-client': true, + 'x-error-code': 'SomeError', + 'x-error-message': 'I am going to retry..', + 'x-error-type': 'INTERNAL_SERVER_ERROR', + }), + }), + }), + ) + }) + + spyRetry.mockClear() + handler.throwThisError( + new SomeOtherError( + 'I am going to retry, notify client turned off..', + ).setNotifyClient(false), + ) + + const fooCustomDlqEvtName = `foo_custom_dlq_evt_${Date.now()}` + + await publish.getPublisher(BarCustomDLQMsg)({ + name: fooCustomDlqEvtName, + }) + + await vi.waitFor(() => { + expect(spyBarDlqCustom).toHaveBeenCalledTimes(1) + expect(spyRetry).toHaveBeenCalledTimes(1) + expect(spyRetry).toHaveBeenNthCalledWith( + 1, + { name: fooCustomDlqEvtName }, + expect.anything(), + ) + + expect(spyRetry.mock.calls.at(0)![1]).toEqual( + expect.objectContaining({ + fields: expect.objectContaining({ + routingKey: 'retry', + }), + + properties: expect.objectContaining({ + headers: expect.objectContaining({ + 'x-original-exchange': 'event-bar-dlq-custom-deadletter', + 'x-original-routing-key': 'bar-custom-dlq', + 'x-max-retries': + constants.CONNECTION_DEFAULT_OPTONS.maxMessageRetryCount, + 'x-event-type': 'event-bar-dlq-custom-deadletter', + 'x-dead-letter-exchange': 'dead.bar-died', + 'x-dead-letter-routing-key': `dead.${helper.getServiceName()}.event-bar-dlq-custom-deadletter.bar-custom-dlq`, + 'x-notify-client': false, + 'x-error-code': 'SomeOtherError', + 'x-error-message': + 'I am going to retry, notify client turned off..', + 'x-error-type': 'BAD_REQUEST', + }), + }), + }), + ) + }) + }) + + test('A message should be immediately sent to default DQL when rejected via RejectMsgError', async () => { + const spyRetry = vi.spyOn(handler, 'listenOnRetry') + const spyFooDlq = vi.spyOn(handler, 'handleFooDqlMsg') + const spyDefaultDlq = vi.spyOn(handler, 'listenOnDefaultDlq') + const spyCustomDlq = vi.spyOn(handler, 'listenOnCustomDlq') + + const fooEvtName = `foo_evt_${Date.now()}` + + handler.throwThisError( + new errors.RejectMsgError('I am going directly to default DLQ'), + ) + await publish.getPublisher(FooDlqMsq)( + { name: fooEvtName }, + { routingKey: 'foo' }, + ) + + await vi.waitFor(() => { + expect(spyFooDlq).toHaveBeenCalledTimes(1) + // running tests in parallel can cause more than 1 message to be sent to default DLQ + expect(spyDefaultDlq).toHaveBeenCalled() + expect(spyRetry).toHaveBeenCalledTimes(0) + expect(spyCustomDlq).toHaveBeenCalledTimes(0) + + const dlqMsg = spyDefaultDlq.mock.calls.find( + (msg) => (<{ name: string }>msg[0]).name === fooEvtName, + ) + + expect(dlqMsg).toBeDefined() + + expect(dlqMsg![1]).toEqual( + expect.objectContaining({ + fields: expect.objectContaining({ + exchange: constants.MANAGED_EXCHANGES.DEAD_LETTER.EXCHANGE, + routingKey: `dead.${helper.getServiceName()}.event-foo-dlq`, + }), + properties: expect.objectContaining({ + headers: expect.objectContaining({ + 'x-current-service-id': helper.getServiceName(), + 'x-event-type': 'event-foo-dlq', + 'x-first-death-exchange': 'event-foo-dlq', + 'x-first-death-reason': 'rejected', + 'x-instance-id': helper.getHostName(), + 'x-origin-service-id': helper.getServiceName(), + }), + }), + }), + ) + }) + }) + + test('A message should be immediately sent to custom DQL when rejected via RejectMsgError', async () => { + const spyRetry = vi.spyOn(handler, 'listenOnRetry') + const spyBarCustomDlq = vi.spyOn(handler, 'handleBarCustomDlqMsg') + const spyDefaultDlq = vi.spyOn(handler, 'listenOnDefaultDlq') + const spyCustomDlq = vi.spyOn(handler, 'listenOnCustomDlq') + + const barEvtName = `bar_evt_${Date.now()}` + + handler.throwThisError( + new errors.RejectMsgError('I am going directly to custom DLQ'), + ) + await publish.getPublisher(BarCustomDLQMsg)({ name: barEvtName }) + + await vi.waitFor(() => { + expect(spyBarCustomDlq).toHaveBeenCalledTimes(1) + expect(spyCustomDlq).toHaveBeenCalledTimes(1) + expect(spyDefaultDlq).toHaveBeenCalledTimes(0) + expect(spyRetry).toHaveBeenCalledTimes(0) + + expect(spyCustomDlq.mock.calls.at(0)![1]).toEqual( + expect.objectContaining({ + fields: expect.objectContaining({ + exchange: 'dead.bar-died', + routingKey: `dead.${helper.getServiceName()}.event-bar-dlq-custom-deadletter.bar-custom-dlq`, + }), + properties: expect.objectContaining({ + headers: expect.objectContaining({ + 'x-current-service-id': helper.getServiceName(), + 'x-event-type': 'event-bar-dlq-custom-deadletter', + 'x-first-death-exchange': 'event-bar-dlq-custom-deadletter', + 'x-first-death-reason': 'rejected', + 'x-instance-id': helper.getHostName(), + 'x-origin-service-id': helper.getServiceName(), + }), + }), + }), + ) + }) + }) +}) diff --git a/test/e2e/subscription.spec.ts b/test/e2e/subscription.spec.ts new file mode 100644 index 0000000..f6f0fb2 --- /dev/null +++ b/test/e2e/subscription.spec.ts @@ -0,0 +1,153 @@ +import { IsString } from 'class-validator' +import { + AmqpMessage, + Message, + MessageHandler, + ResourceMessage, + SnapshotMessageHandler, + SnapshotRequested, + SubscribeInternal, + publish, + subscription, +} from '../../src' +import { irisTesting } from '../setup' + +@Message({ name: 'event-foo' }) +class Foo { + @IsString() name!: string +} + +@Message({ name: 'do-subscribe-me' }) +class DoSubscribeMe { + @IsString() toResource!: string + @IsString() toId!: string +} + +class Handler { + @MessageHandler() + async subMe(evt: DoSubscribeMe, irisMsg: AmqpMessage): Promise { + await subscription.internallySubscribe(irisMsg, evt.toResource, evt.toId) + } + + /** + * this is managed by a dedicated subscription service, using + * handler here just for spy purposes + */ + @MessageHandler() + onSubscribeInternal(_evt: SubscribeInternal): void {} + + /** + * this is managed by a dedicated subscription service, using + * handler here just for spy purposes + */ + @MessageHandler({ bindingKeys: '*.resource' }) + onSubscription(_evt: ResourceMessage): void {} + + @MessageHandler({ bindingKeys: 'foo-resource' }) + onSnapshotRequestFooViaClassicDecorator(_evt: SnapshotRequested): void {} + + @SnapshotMessageHandler({ resourceType: 'bar-resource' }) + onSnapshotRequestBar(_evt: SnapshotRequested): void {} + + @SnapshotMessageHandler({ resourceType: 'car-resource' }) + onSnapshotRequestCar(_evt: SnapshotRequested): void {} +} + +describe('Subscription', () => { + let suite: irisTesting.integration.RegisterAndConnectReturnT + let handler: Handler + + beforeAll(async () => { + suite = await irisTesting.integration.registerAndConnect(Handler) + handler = suite.getHandlerFor(Handler)! + }) + + afterEach(async () => { + await suite.clearQueues() + }) + + afterAll(async () => { + await suite.deleteQueues() + }) + + test('getSubscriptionPublisher()', async () => { + const spyOnSubscription = vi.spyOn(handler, 'onSubscription') + + const subPublisher = subscription.getSnapshotPublisher(Foo) + await subPublisher({ name: 'foo_evt 1' }, 'foo', 'foo-1') + await subPublisher({ name: 'foo_evt 2' }, 'foo', 'foo-2') + + await vi.waitFor(() => { + expect(spyOnSubscription).toHaveBeenCalledTimes(2) + expect(spyOnSubscription).toHaveBeenNthCalledWith(1, { + payload: { name: 'foo_evt 1' }, + resourceType: 'foo', + resourceId: 'foo-1', + }) + expect(spyOnSubscription).toHaveBeenNthCalledWith(2, { + payload: { name: 'foo_evt 2' }, + resourceType: 'foo', + resourceId: 'foo-2', + }) + }) + }) + + test('internallySubscribe() called as side effect of another event', async () => { + const spyOnSubscribeInternal = vi.spyOn(handler, 'onSubscribeInternal') + const spyOnSubscribeMe = vi.spyOn(handler, 'subMe') + + await publish.getPublisher(DoSubscribeMe)({ + toResource: 'some-resource', + toId: '1-2-3', + }) + + await vi.waitFor(() => { + expect(spyOnSubscribeMe).toHaveBeenCalledTimes(1) + expect(spyOnSubscribeInternal).toHaveBeenCalledTimes(1) + + expect(spyOnSubscribeInternal).toHaveBeenNthCalledWith(1, { + resourceType: 'some-resource', + resourceId: '1-2-3', + }) + }) + }) + + test('SnapshotMessageHandler()', async () => { + const spyOnSnapshotBar = vi.spyOn(handler, 'onSnapshotRequestBar') + const spyOnSnapshotCar = vi.spyOn(handler, 'onSnapshotRequestCar') + + await irisTesting.utilities.requestSnapshot('bar-resource', 'bar-id') + await irisTesting.utilities.requestSnapshot('car-resource', 'car-id') + + await vi.waitFor(() => { + expect(spyOnSnapshotBar).toHaveBeenCalledTimes(1) + expect(spyOnSnapshotBar).toHaveBeenNthCalledWith(1, { + resourceType: 'bar-resource', + resourceId: 'bar-id', + }) + + expect(spyOnSnapshotCar).toHaveBeenCalledTimes(1) + expect(spyOnSnapshotCar).toHaveBeenNthCalledWith(1, { + resourceType: 'car-resource', + resourceId: 'car-id', + }) + }) + }) + + test('SnapshotRequested via MessageHandler()', async () => { + const spyOnSnapshotFoo = vi.spyOn( + handler, + 'onSnapshotRequestFooViaClassicDecorator', + ) + + await irisTesting.utilities.requestSnapshot('foo-resource', 'foo-id') + + await vi.waitFor(() => { + expect(spyOnSnapshotFoo).toHaveBeenCalledTimes(1) + expect(spyOnSnapshotFoo).toHaveBeenNthCalledWith(1, { + resourceType: 'foo-resource', + resourceId: 'foo-id', + }) + }) + }) +}) diff --git a/test/e2e/validation/message_handler_validation.spec.ts b/test/e2e/validation/message_handler_validation.spec.ts new file mode 100644 index 0000000..9f2d1fc --- /dev/null +++ b/test/e2e/validation/message_handler_validation.spec.ts @@ -0,0 +1,236 @@ +import { IsString } from 'class-validator' +import { + ExchangeType, + Message, + MessageDeliveryMode, + MessageHandler, + Scope, + SnapshotRequested, + connection, + publish, +} from '../../../src' +import { irisTesting } from '../../setup' + +class NonMessageClass {} + +@Message({ name: 'foo', scope: Scope.FRONTEND }) +class FooFrontend {} + +@Message({ name: 'foo-topic', exchangeType: ExchangeType.topic }) +class FooTopic {} + +@Message({ name: 'foo-direct', exchangeType: ExchangeType.direct }) +class FooDirect { + @IsString() msg!: string +} + +@Message({ name: 'foo-fanout' }) +class FooFanout {} + +describe('MessageHandler validation', () => { + test('MessageDeliveryMode = PER_SERVICE_INSTANCE is not allowed for FRONTEND scope messages', async () => { + const expectedError = { + message: expect.stringMatching( + /ERR_IRIS_PER_SERVICE_INSTANCE_NOT_SUPPORTED_FOR_FRONTEND_SCOPE/, + ), + } + + class Handler { + @MessageHandler({ + messageDeliveryMode: MessageDeliveryMode.PER_SERVICE_INSTANCE, + }) + handle(_evt: FooFrontend): void {} + } + await expect( + irisTesting.integration.registerAndConnect(Handler), + ).rejects.toMatchObject(expectedError) + }) + + test('MessageDeliveryMode = PER_SERVICE_INSTANCE is not allowed for SubscriptionSnapshotRequested messages', async () => { + const expectedError = { + message: expect.stringMatching( + /ERR_IRIS_PER_SERVICE_INSTANCE_NOT_SUPPORTED_WITH_SNAPSHOT_REQUESTED_MESSAGE/, + ), + } + + class Handler { + @MessageHandler({ + messageDeliveryMode: MessageDeliveryMode.PER_SERVICE_INSTANCE, + }) + handle(_evt: SnapshotRequested): void {} + } + await expect( + irisTesting.integration.registerAndConnect(Handler), + ).rejects.toMatchObject(expectedError) + }) + + test('Setting bindingKeys for handler using FRONTEND scope message is not supported', async () => { + const expectedError = { + message: expect.stringMatching( + /ERR_IRIS_MESSAGE_HANDLER_BINDING_KEYS_ARE_OVERRIDDEN_FOR_FRONTEND_SCOPE/, + ), + } + + class Handler { + @MessageHandler({ bindingKeys: 'some-binding-key' }) + handle(_evt: FooFrontend): void {} + } + await expect( + irisTesting.integration.registerAndConnect(Handler), + ).rejects.toMatchObject(expectedError) + }) + + test('Setting bindingKeys for handler using FANOUT messages is not supported', async () => { + const expectedError = { + message: expect.stringMatching( + /ERR_IRIS_MESSAGE_HANDLER_BINDING_KEYS_HAVE_NO_EFFECT_FOR_FANOUT_EXCHANGE/, + ), + } + + class Handler { + @MessageHandler({ bindingKeys: 'some-binding-key' }) + handle(_evt: FooFanout): void {} + } + await expect( + irisTesting.integration.registerAndConnect(Handler), + ).rejects.toMatchObject(expectedError) + }) + + test('Binding keys should be kebab-case only for DIRECT messages', async () => { + const expectedError = { + message: expect.stringMatching( + /ERR_IRIS_MESSAGE_HANDLER_INVALID_BINDING_KEYS/, + ), + } + + class HandlerFanout { + @MessageHandler({ bindingKeys: 'some-binding.key' }) + handle(_evt: FooDirect): void {} + } + await expect( + irisTesting.integration.registerAndConnect(HandlerFanout), + ).rejects.toMatchObject(expectedError) + }) + + test('Binding keys should be kebab-case for TOPIC messages', async () => { + const expectedError = { + message: expect.stringMatching( + /ERR_IRIS_MESSAGE_HANDLER_INVALID_BINDING_KEYS/, + ), + } + + class Handler { + @MessageHandler({ bindingKeys: 'some_binding.key' }) + handle(_evt: FooTopic): void {} + } + await expect( + irisTesting.integration.registerAndConnect(Handler), + ).rejects.toMatchObject(expectedError) + }) + + test('Event Class should be decorated with @Message', () => { + const expectedError = /ERR_IRIS_INVALID_HANDLER_CONFIG/ + expect(() => { + class Handler { + @MessageHandler() + handle(_evt: NonMessageClass): void {} + } + }).toThrowError(expectedError) + }) + + test('Only @Message is allowed as handler argument', () => { + const expectedError = /ERR_IRIS_INVALID_HANDLER_CONFIG/ + + expect(() => { + class Handler { + @MessageHandler() + handle(_evt: FooTopic, _evt2: FooDirect): void {} + } + }).toThrowError(expectedError) + }) + + test('Reply Class should be decorated with @Message', () => { + const expectedError = /ERR_IRIS_INVALID_HANDLER_REPLY_CLASS/ + expect(() => { + class Handler { + @MessageHandler({}, NonMessageClass) + handle(_evt: FooFanout): void {} + } + }).toThrowError(expectedError) + }) + + test('Using handler for same @Message with same bindingKeys is invalid', async () => { + const expectedError = { + message: expect.stringMatching(/ERR_IRIS_DUPLICATE_MESSAGE_HANDLER/), + } + + class HandlerWithBKeys { + @MessageHandler({ bindingKeys: ['foo', 'bar'] }) + handleFoo(_evt: FooTopic): void {} + + @MessageHandler({ bindingKeys: ['foo', 'bar'] }) + handleFoo2(_evt: FooTopic): void {} + } + await expect( + irisTesting.integration.registerAndConnect(HandlerWithBKeys), + ).rejects.toMatchObject(expectedError) + + class HandlerWithDefaults { + @MessageHandler() + handleFoo(_evt: FooFanout): void {} + + @MessageHandler() + handleFoo2(_evt: FooFanout): void {} + } + await expect( + irisTesting.integration.registerAndConnect(HandlerWithDefaults), + ).rejects.toMatchObject(expectedError) + }) + + test('Using handler for same @Message but different bindingKeys should work', async () => { + class Handler { + @MessageHandler({ bindingKeys: 'foo' }) + handleFooKey(_evt: FooDirect): void {} + + @MessageHandler({ bindingKeys: 'bar' }) + handleBarKey(_evt: FooDirect): void {} + } + const suite = await irisTesting.integration.registerAndConnect(Handler) + + const handler = suite.getHandlerFor(Handler)! + const publisher = publish.getPublisher(FooDirect) + + const spyFoo = vi.spyOn(handler, 'handleFooKey') + const spyBar = vi.spyOn(handler, 'handleBarKey') + + await publisher({ msg: 'foo' }, { routingKey: 'foo' }) + await publisher({ msg: 'bar' }, { routingKey: 'bar' }) + await vi.waitFor(() => { + expect(spyFoo).toHaveBeenCalledTimes(1) + expect(spyFoo).toHaveBeenNthCalledWith(1, { msg: 'foo' }) + + expect(spyBar).toHaveBeenCalledTimes(1) + expect(spyBar).toHaveBeenNthCalledWith(1, { msg: 'bar' }) + }) + + await suite.deleteQueues() + await connection.disconnect() + }) + + test('When using DIRECT Message, exactly one bindingKey should be set', async () => { + const expectedError = { + message: expect.stringMatching( + /ERR_IRIS_MESSAGE_HANDLER_INVALID_BINDING_KEYS/, + ), + } + + class HandlerMultipleBindingKeys { + @MessageHandler({ bindingKeys: ['foo', 'bar'] }) + handle(_evt: FooDirect): void {} + } + + await expect( + irisTesting.integration.registerAndConnect(HandlerMultipleBindingKeys), + ).rejects.toMatchObject(expectedError) + }) +}) diff --git a/test/e2e/validation/message_validation.spec.ts b/test/e2e/validation/message_validation.spec.ts new file mode 100644 index 0000000..ed7055d --- /dev/null +++ b/test/e2e/validation/message_validation.spec.ts @@ -0,0 +1,100 @@ +import { Message, MessageHandler } from '../../../src' +import { irisTesting } from '../../setup' + +describe('Message Validation', () => { + test("Non empty 'name' is required for @Message", async () => { + const expectedError = { + message: expect.stringMatching(/ERR_IRIS_MESSAGE_REQUIRES_NAME/), + } + + @Message({ name: '' }) + class Foo {} + + class Handler { + @MessageHandler() + handle(_evt: Foo): void {} + } + + await expect( + irisTesting.integration.registerAndConnect(Handler), + ).rejects.toMatchObject(expectedError) + }) + + test('Name should be kebab-case', async () => { + const expectedError = { + message: expect.stringMatching(/ERR_IRIS_MESSAGE_INVALID_NAME/), + } + + @Message({ name: 'foo+msg' }) + class Foo {} + + class Handler { + @MessageHandler() + handle(_evt: Foo): void {} + } + + await expect( + irisTesting.integration.registerAndConnect(Handler), + ).rejects.toMatchObject(expectedError) + }) + + test('RoutingKey should be kebab-case', async () => { + const expectedError = { + message: expect.stringMatching(/ERR_IRIS_MESSAGE_INVALID_ROUTING_KEY/), + } + + @Message({ name: 'foo', routingKey: 'foo+evt' }) + class Foo {} + + class Handlere { + @MessageHandler() + handle(_evt: Foo): void {} + } + + await expect( + irisTesting.integration.registerAndConnect(Handlere), + ).rejects.toMatchObject(expectedError) + }) + + test('DeadLetter should be kebab-case', async () => { + const expectedError = { + message: expect.stringMatching( + /ERR_IRIS_MESSAGE_INVALID_DEAD_LETTER_NAME/, + ), + } + + @Message({ name: 'foo', deadLetter: 'foo+evt' }) + class Foo {} + + class Handlere { + @MessageHandler() + handle(_evt: Foo): void {} + } + + await expect( + irisTesting.integration.registerAndConnect(Handlere), + ).rejects.toMatchObject(expectedError) + }) + + test('Exchange names should be unique (should not allow multiple @Message classes with same name)', async () => { + const expectedError = { + message: expect.stringMatching(/ERR_IRIS_MESSAGE_NAME_NOT_UNIQUE/), + } + + @Message({ name: 'foo' }) + class Foo {} + + @Message({ name: 'foo' }) + class AnotherFoo {} + + class Handler { + @MessageHandler() + handle(_evt: Foo): void {} + @MessageHandler() + handleAnother(_evt: AnotherFoo): void {} + } + await expect( + irisTesting.integration.registerAndConnect(Handler), + ).rejects.toMatchObject(expectedError) + }) +}) diff --git a/test/helpers.ts b/test/helpers.ts new file mode 100644 index 0000000..7e1f11e --- /dev/null +++ b/test/helpers.ts @@ -0,0 +1,13 @@ +export class Pausable { + protected paused: Promise = Promise.resolve() + public resume: (value?: Promise) => void = () => {} + + public pause(): void { + this.resume( + // biome-ignore lint/suspicious/noAssignInExpressions: + (this.paused = new Promise((resolve) => { + this.resume = resolve + })), + ) + } +} diff --git a/test/rabbit.ts b/test/rabbit.ts new file mode 100644 index 0000000..6b26a30 --- /dev/null +++ b/test/rabbit.ts @@ -0,0 +1,123 @@ +import { randomUUID } from 'node:crypto' + +const RABBIT_ADMIN_PORT = '15672' +const RABBIT_TEST_USER_PREFIX = 'rtestuser_' + +export function getAmqpUrl() { + return process.env.AMQP_URL! +} + +export const adminGetConnectionNames = async ( + amqpUrl = getAmqpUrl(), +): Promise => { + const [url, opts] = getAdminFetchOpts(`${amqpUrl}/api/connections`) + const res = await (await fetch(url, opts)).json() + + return (<{ name: string }[]>res).map(({ name }) => name) +} + +export const adminCloseAllConnections = async () => { + const connections = await adminGetConnectionNames() + + await Promise.all(connections.map(async (cn) => adminCloseConnection(cn))) +} + +export const adminCloseConnection = async ( + connectionName: string, + amqpUrl = getAmqpUrl(), +) => { + const [url, opts] = getAdminFetchOpts( + `${amqpUrl}/api/connections/${encodeURIComponent(connectionName)}`, + ) + + await fetch(url, { ...opts, method: 'DELETE' }) +} + +export const adminUpsertUser = async ( + user?: string, + pass?: string, + amqpUrl = getAmqpUrl(), +): Promise<{ username: string; password: string }> => { + const username = user ?? `${RABBIT_TEST_USER_PREFIX}${randomUUID()}` + const usernameParam = encodeURIComponent(username) + const vhostParam = encodeURIComponent('/') + const password = pass ?? randomUUID() + + const [url, opts] = getAdminFetchOpts(`${amqpUrl}/api/users/${usernameParam}`) + await fetch(url, { + ...opts, + method: 'PUT', + body: JSON.stringify({ + username, + password, + tags: 'tests', + }), + }) + + await fetch(`${url.origin}/api/permissions/${vhostParam}/${usernameParam}`, { + ...opts, + method: 'PUT', + body: JSON.stringify({ + username, + vhost: '/', + configure: '.*', + write: '.*', + read: '.*', + }), + }) + + return { username, password } +} + +export const adminDeleteAllTestUsers = async ( + amqpUrl = getAmqpUrl(), +): Promise => { + const [url, opts] = getAdminFetchOpts(`${amqpUrl}/api/users`) + + const users = await (await fetch(url, opts)).json() + + await Promise.all( + (<{ name: string }[]>users) + .filter(({ name }) => name.startsWith(RABBIT_TEST_USER_PREFIX)) + .map(async ({ name }) => adminDeleteUser(name, amqpUrl)), + ) +} + +export const adminDeleteUser = async ( + username: string, + amqpUrl = getAmqpUrl(), +): Promise => { + const usernameParam = encodeURIComponent(username) + const [url, opts] = getAdminFetchOpts(`${amqpUrl}/api/users/${usernameParam}`) + + await fetch(url, { ...opts, method: 'DELETE' }) +} + +const getAdminFetchOpts = ( + amqpUrl = getAmqpUrl(), +): [URL, Record] => { + const url = new URL(amqpUrl) + url.port = RABBIT_ADMIN_PORT + + const urlNoCreds = new URL( + // proto can not be chaned via URL class + // https://nodejs.org/api/url.html#special-schemes + url + .toString() + .replace('amqps:', 'https:') + .replace('amqp:', 'http:'), + ) + + urlNoCreds.username = '' + urlNoCreds.password = '' + + return [urlNoCreds, getBasicAuthHeaders(url.username, url.password)] +} + +function getBasicAuthHeaders(username: string, password: string) { + return { + headers: { + Authorization: `Basic ${btoa(`${username}:${password}`)}`, + }, + } +} diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..21736ba --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,10 @@ +import * as iris from '../src' +import * as testing from '../src/testing' + +iris.helper.setServiceName('iris_node_tests') +iris.flags.ALLOW_USING_RESERVED_NAMES = true +iris.flags.ASSERT_NON_INTERNAL_EXCHANGES = true + +testing.utilities.setLogLevel() + +export { testing as irisTesting } diff --git a/testing/package.json b/testing/package.json new file mode 100644 index 0000000..ef1e691 --- /dev/null +++ b/testing/package.json @@ -0,0 +1,5 @@ +{ + "name": "@iris-events/iris/testing", + "typings": "../dist/testing/index.d.ts", + "main": "../dist/testing/index.js" +} diff --git a/tsconfig.build.json b/tsconfig.build.json index 6ec7b56..b3140e9 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -3,5 +3,5 @@ "compilerOptions": { "rootDir": "./src" }, - "include": ["src/index.ts", "src/lib/asyncapi/index.ts"] + "include": ["src/index.ts", "src/lib/asyncapi/index.ts", "src/testing/index.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 0d1cfe2..0076ce8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,39 +1,46 @@ { + "$schema": "http://json.schemastore.org/tsconfig", "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "baseUrl": ".", "target": "es2022", "module": "commonjs", - "lib": [ - "es2022", - "es7", - "es6" - ], + "moduleResolution": "node", + "lib": ["es2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, "declaration": true, "declarationMap": true, "sourceMap": true, - "outDir": "./dist", - "rootDir": ".", - "baseUrl": "./", - "strict": true, - "noImplicitAny": false, - "strictNullChecks": true, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, + + "incremental": false, + "noImplicitReturns": false, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noEmitOnError": true, + "allowUnusedLabels": false, + "noUnusedLocals": false, + "noUnusedParameters": true, + "strictNullChecks": true, + "noImplicitAny": false, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false, + "types": [ + "vitest/globals", + ], "paths": { "*": [ + "src/*", + "dist/*", "types/*", "node_modules/*" ] } }, - "include": [ - "src/**/*", - "test/**/*", - "examples/**/*" - ], - "exclude": [ - "node_modules", - "dist" - ] + "include": ["src/**/*", "test/**/*"], + "exclude": ["node_modules", "dist"] } diff --git a/vitest.config.mts b/vitest.config.mts new file mode 100644 index 0000000..390e612 --- /dev/null +++ b/vitest.config.mts @@ -0,0 +1,42 @@ +import { defineConfig } from 'vitest/config' +import swc from 'unplugin-swc' + +export default defineConfig({ + test: { + setupFiles: ['dotenv/config'], + cache: false, + globals: true, + root: './', + clearMocks: true, + reporters: ['verbose'], + include: [ + '(src|test)/**/*.spec.ts' + ], + fileParallelism: true, + coverage: { + clean: true, // clean existing before running test + enabled: true, + reporter: ['html-spa', 'text'], + provider: 'v8', + thresholds: { + branches: 80, + functions: 80, + lines: 80, + statements: 80, + }, + reportsDirectory: 'coverage/all', + exclude: [ + '**/coverage/**', + '**/dist/**', + '**/test?(s)/**', + '**/vitest*', + '**/jest*', + ] + }, + }, + plugins: [ + swc.vite({ + module: { type: 'nodenext' }, + }) + ] +})