├── .github ├── release.yml └── workflows │ ├── lint.yml │ ├── npm-publish-specific-package-not-main.yml │ ├── publish-not-main.yml │ ├── publish.yml │ └── unit.tests.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── eslintrc.json ├── jest.config.json ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── auth │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ ├── decorators │ │ │ ├── index.ts │ │ │ ├── jwt.ts │ │ │ ├── native.auth.ts │ │ │ └── no.auth.ts │ │ ├── errors │ │ │ └── native.auth.invalid.origin.error.ts │ │ ├── index.ts │ │ ├── jwt.admin.guard.ts │ │ ├── jwt.authenticate.guard.ts │ │ ├── jwt.or.native.auth.guard.ts │ │ ├── native.auth.admin.guard.ts │ │ └── native.auth.guard.ts │ ├── test │ │ ├── .gitkeep │ │ └── native.auth.guard.spec.ts │ └── tsconfig.json ├── cache │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ ├── cache │ │ │ ├── cache.module.ts │ │ │ ├── cache.service.ts │ │ │ └── index.ts │ │ ├── decorators │ │ │ ├── index.ts │ │ │ └── no.cache.ts │ │ ├── entities │ │ │ ├── caching.module.async.options.ts │ │ │ ├── caching.module.options.ts │ │ │ ├── common.ts │ │ │ ├── guest.caching.ts │ │ │ └── local.cache.value.ts │ │ ├── guest-cache │ │ │ ├── guest-cache.middleware.ts │ │ │ ├── guest-cache.service.ts │ │ │ ├── guest-cache.warmer.ts │ │ │ └── index.ts │ │ ├── in-memory-cache │ │ │ ├── entities │ │ │ │ ├── common.constants.ts │ │ │ │ └── in-memory-cache-options.interface.ts │ │ │ ├── in-memory-cache.module.ts │ │ │ ├── in-memory-cache.service.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── interceptors │ │ │ ├── caching.interceptor.ts │ │ │ └── guest.cache.interceptor.ts │ │ ├── jitter.ts │ │ ├── local.cache.service.ts │ │ ├── redis-cache │ │ │ ├── index.ts │ │ │ ├── options.ts │ │ │ ├── redis-cache.module.ts │ │ │ └── redis-cache.service.ts │ │ └── redlock │ │ │ ├── entities │ │ │ ├── index.ts │ │ │ ├── redlock.connection.async.options.ts │ │ │ ├── redlock.connection.options.ts │ │ │ └── redlock.log.level.ts │ │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── lock.timeout.error.ts │ │ │ ├── index.ts │ │ │ ├── redlock.configuration.ts │ │ │ ├── redlock.constants.ts │ │ │ ├── redlock.module.ts │ │ │ └── redlock.service.ts │ ├── test │ │ └── .gitkeep │ └── tsconfig.json ├── common │ ├── .eslintrc.js │ ├── package.json │ ├── src │ │ ├── common │ │ │ ├── complexity │ │ │ │ ├── apply.complexity.ts │ │ │ │ ├── complexity.estimation.ts │ │ │ │ ├── complexity.node.ts │ │ │ │ ├── complexity.tree.ts │ │ │ │ ├── complexity.utils.ts │ │ │ │ └── exceptions │ │ │ │ │ ├── complexity.exceeded.exception.ts │ │ │ │ │ └── parent.node.not.found.exception.ts │ │ │ ├── config │ │ │ │ ├── base.config.service.ts │ │ │ │ ├── base.config.utils.ts │ │ │ │ ├── configuration.loader.error.ts │ │ │ │ ├── configuration.loader.schema.expander.ts │ │ │ │ ├── configuration.loader.schema.type.ts │ │ │ │ ├── configuration.loader.settings.ts │ │ │ │ ├── configuration.loader.ts │ │ │ │ ├── index.ts │ │ │ │ └── mxnest.config.service.ts │ │ │ ├── entities │ │ │ │ └── amount.ts │ │ │ ├── logging │ │ │ │ └── logging.module.ts │ │ │ ├── shutdown-aware │ │ │ │ ├── index.ts │ │ │ │ ├── shutdown-aware.handler.ts │ │ │ │ ├── shutdown-aware.ts │ │ │ │ └── shutting-down.error.ts │ │ │ └── swappable-settings │ │ │ │ ├── entities │ │ │ │ ├── constants.ts │ │ │ │ ├── swappable-settings-async-options.interface.ts │ │ │ │ └── swappable-settings-storage.interface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── swappable-settings.controller.ts │ │ │ │ ├── swappable-settings.module.ts │ │ │ │ └── swappable-settings.service.ts │ │ ├── decorators │ │ │ ├── error.logger.ts │ │ │ ├── index.ts │ │ │ ├── lock.ts │ │ │ ├── passthrough.ts │ │ │ └── swappable-setting.ts │ │ ├── index.ts │ │ ├── pipes │ │ │ ├── entities │ │ │ │ └── parse.array.options.ts │ │ │ ├── parse.address.and.metachain.pipe.ts │ │ │ ├── parse.address.array.pipe.ts │ │ │ ├── parse.address.pipe.ts │ │ │ ├── parse.array.pipe.ts │ │ │ ├── parse.block.hash.pipe.ts │ │ │ ├── parse.bls.hash.pipe.ts │ │ │ ├── parse.bool.pipe.ts │ │ │ ├── parse.collection.array.pipe.ts │ │ │ ├── parse.collection.pipe.ts │ │ │ ├── parse.enum.array.pipe.ts │ │ │ ├── parse.enum.pipe.ts │ │ │ ├── parse.hash.array.pipe.ts │ │ │ ├── parse.hash.pipe.ts │ │ │ ├── parse.int.pipe.ts │ │ │ ├── parse.nft.array.pipe.ts │ │ │ ├── parse.nft.pipe.ts │ │ │ ├── parse.record.pipe.ts │ │ │ ├── parse.regex.pipe.ts │ │ │ ├── parse.token.or.nft.pipe.ts │ │ │ ├── parse.token.pipe.ts │ │ │ ├── parse.transaction.hash.array.pipe.ts │ │ │ └── parse.transaction.hash.pipe.ts │ │ ├── sc.interactions │ │ │ ├── contract.loader.ts │ │ │ ├── contract.query.runner.ts │ │ │ ├── contract.transaction.generator.ts │ │ │ └── index.ts │ │ └── utils │ │ │ ├── address.utils.ts │ │ │ ├── batch.utils.ts │ │ │ ├── binary.utils.ts │ │ │ ├── constants.ts │ │ │ ├── context.tracker.ts │ │ │ ├── date.utils.ts │ │ │ ├── decorator.utils.ts │ │ │ ├── execution.context.utils.ts │ │ │ ├── extensions │ │ │ ├── array.extensions.ts │ │ │ ├── date.extensions.ts │ │ │ ├── jest.extensions.ts │ │ │ ├── number.extensions.ts │ │ │ └── string.extensions.ts │ │ │ ├── file.utils.ts │ │ │ ├── locker.ts │ │ │ ├── logger.initializer.ts │ │ │ ├── match.utils.ts │ │ │ ├── mxnest.constants.ts │ │ │ ├── number.utils.ts │ │ │ ├── origin.logger.ts │ │ │ ├── pending.executer.ts │ │ │ ├── record.utils.ts │ │ │ ├── round.utils.ts │ │ │ ├── string.utils.ts │ │ │ ├── swagger.utils.ts │ │ │ ├── token.utils.ts │ │ │ └── url.utils.ts │ ├── test │ │ ├── config │ │ │ └── base.config.service.spec.ts │ │ ├── extensions │ │ │ ├── array.extensions.spec.ts │ │ │ ├── date.extensions.spec.ts │ │ │ ├── number.extensions.spec.ts │ │ │ └── string.extensions.spec.ts │ │ ├── number.extensions.spec.ts │ │ ├── pipes │ │ │ ├── parse.address.pipe.spec.ts │ │ │ ├── parse.bool.pipe.spec.ts │ │ │ ├── parse.collection.array.pipe.spec.ts │ │ │ └── parse.nft.array.pipe.spec.ts │ │ ├── sc.interactions │ │ │ ├── contract.loader.spec.ts │ │ │ ├── contract.transaction.generator.spec.ts │ │ │ └── test.abi.json │ │ ├── string.extensions.spec.ts │ │ └── utils │ │ │ ├── address.utils.spec.ts │ │ │ ├── batch.utils.spec.ts │ │ │ ├── binary.utils.spec.ts │ │ │ ├── const.utils.spec.ts │ │ │ ├── date.utils.spec.ts │ │ │ ├── match.utils.spec.ts │ │ │ ├── round.utils.spec.ts │ │ │ ├── string.utils.spec.ts │ │ │ ├── token.utils.spec.ts │ │ │ └── url.utils.spec.ts │ └── tsconfig.json ├── elastic │ ├── .eslintrc.js │ ├── package.json │ ├── src │ │ ├── elastic.module.ts │ │ ├── elastic.service.ts │ │ ├── entities │ │ │ ├── abstract.query.ts │ │ │ ├── elastic.module.async.options.ts │ │ │ ├── elastic.module.options.ts │ │ │ ├── elastic.pagination.ts │ │ │ ├── elastic.query.ts │ │ │ ├── elastic.sort.order.ts │ │ │ ├── elastic.sort.property.ts │ │ │ ├── exists.query.ts │ │ │ ├── match.query.ts │ │ │ ├── must.query.ts │ │ │ ├── nested.query.ts │ │ │ ├── nested.should.query.ts │ │ │ ├── query.condition.options.ts │ │ │ ├── query.condition.ts │ │ │ ├── query.operator.ts │ │ │ ├── query.range.ts │ │ │ ├── query.type.ts │ │ │ ├── range.greater.than.or.equal.ts │ │ │ ├── range.greater.than.ts │ │ │ ├── range.lower.than.or.equal.ts │ │ │ ├── range.lower.than.ts │ │ │ ├── range.query.ts │ │ │ ├── script.query.ts │ │ │ ├── should.query.ts │ │ │ ├── string.query.ts │ │ │ ├── terms.query.ts │ │ │ └── wildcard.query.ts │ │ └── index.ts │ ├── test │ │ ├── .gitkeep │ │ └── elastic.utils.spec.ts │ └── tsconfig.json ├── http │ ├── .eslintrc.js │ ├── package.json │ ├── src │ │ ├── api.utils.ts │ │ ├── api │ │ │ ├── api.module.ts │ │ │ ├── api.service.ts │ │ │ ├── entities │ │ │ │ ├── api.module.async.options.ts │ │ │ │ ├── api.module.options.ts │ │ │ │ ├── api.settings.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── auth │ │ │ ├── index.ts │ │ │ └── native.auth.signer.ts │ │ ├── index.ts │ │ └── interceptors │ │ │ ├── cleanup.interceptor.ts │ │ │ ├── complexity.interceptor.ts │ │ │ ├── entities │ │ │ ├── disable.fields.interceptor.on.controller.ts │ │ │ ├── disable.fields.interceptor.ts │ │ │ └── index.ts │ │ │ ├── exclude.fields.interceptor.ts │ │ │ ├── extract.interceptor.ts │ │ │ ├── fields.interceptor.ts │ │ │ ├── index.ts │ │ │ ├── origin.interceptor.ts │ │ │ ├── pagination.interceptor.ts │ │ │ └── query.check.interceptor.ts │ ├── test │ │ ├── api.utils.spec.ts │ │ └── native.auth.signer.spec.ts │ └── tsconfig.json ├── monitoring │ ├── .eslintrc.js │ ├── README.md │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── interceptors │ │ │ ├── index.ts │ │ │ ├── log.requests.interceptor.ts │ │ │ ├── logging.interceptor.ts │ │ │ └── request.cpu.time.interceptor.ts │ │ ├── metrics │ │ │ ├── entities │ │ │ │ └── elastic.metric.type.ts │ │ │ ├── index.ts │ │ │ ├── metrics.module.ts │ │ │ └── metrics.service.ts │ │ └── profilers │ │ │ ├── cpu.profiler.ts │ │ │ ├── index.ts │ │ │ └── performance.profiler.ts │ ├── test │ │ └── .gitkeep │ └── tsconfig.json ├── rabbitmq │ ├── .eslintrc.js │ ├── package.json │ ├── src │ │ ├── entities │ │ │ ├── async-options.interface.ts │ │ │ ├── constants.ts │ │ │ ├── consumer-config.interface.ts │ │ │ ├── index.ts │ │ │ ├── options.interface.ts │ │ │ └── options.ts │ │ ├── index.ts │ │ ├── interceptors │ │ │ └── rabbitmq-consumer-monitoring.interceptor.ts │ │ ├── publisher.service.ts │ │ ├── rabbit-context-checker.service.ts │ │ ├── rabbit.module.ts │ │ └── subscribers.decorators.ts │ ├── test │ │ └── .gitkeep │ └── tsconfig.json └── redis │ ├── .eslintrc.js │ ├── package.json │ ├── src │ ├── entities │ │ └── common.constants.ts │ ├── index.ts │ ├── options.ts │ └── redis.module.ts │ ├── test │ └── .gitkeep │ └── tsconfig.json └── tsconfig.json /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | categories: 6 | - title: Breaking Changes 🛠 7 | labels: 8 | - breaking-change 9 | - title: Bugfixes 🐛 10 | labels: 11 | - bug 12 | - title: Exciting New Features 🎉 13 | labels: 14 | - feature 15 | - title: Other Notable Changes 16 | labels: 17 | - "*" 18 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Lint 5 | 6 | on: 7 | push: 8 | 9 | jobs: 10 | common: 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: . 15 | 16 | strategy: 17 | matrix: 18 | node-version: [20.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: "npm" 28 | cache-dependency-path: ./package-lock.json 29 | - run: npx lerna@6 bootstrap 30 | - run: npx lerna@6 run lint 31 | env: 32 | CI: true 33 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish-specific-package-not-main.yml: -------------------------------------------------------------------------------- 1 | name: Publish (alpha / beta) with specific package 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | channel: 7 | type: choice 8 | description: NPM channel 9 | options: 10 | - alpha 11 | - beta 12 | package_name: 13 | type: string 14 | description: "Optional: Specify the package to publish (leave empty to publish all changed packages)" 15 | required: false 16 | 17 | permissions: 18 | contents: write 19 | 20 | jobs: 21 | publish-npm: 22 | runs-on: ubuntu-latest 23 | defaults: 24 | run: 25 | working-directory: ./ 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions/setup-node@v1 29 | with: 30 | node-version: 16 31 | registry-url: https://registry.npmjs.org/ 32 | cache-dependency-path: ./package-lock.json 33 | 34 | - run: npx lerna@6 bootstrap 35 | - run: npx lerna@6 run build 36 | 37 | - name: Create release (only for all packages) 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | run: | 41 | if [ -z "${{ github.event.inputs.package_name }}" ]; then 42 | echo "Creating a release for all packages..." 43 | RELEASE_TAG=v$(node -p "require('./lerna.json').version") 44 | gh release create --prerelease $RELEASE_TAG --target=$GITHUB_SHA --title="$RELEASE_TAG" --generate-notes 45 | else 46 | echo "Skipping release creation because we're publishing a single package." 47 | fi 48 | 49 | - name: Publish to npmjs 50 | env: 51 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 52 | run: | 53 | if [ -z "${{ github.event.inputs.package_name }}" ]; then 54 | echo "Publishing all changed packages..." 55 | npx lerna publish from-package --yes --dist-tag ${{ github.event.inputs.channel }} --pre-dist-tag ${{ github.event.inputs.channel }} 56 | else 57 | echo "Publishing only: ${{ github.event.inputs.package_name }}" 58 | 59 | # Publish the specific package 60 | npx lerna publish from-package --yes --dist-tag ${{ github.event.inputs.channel }} --pre-dist-tag ${{ github.event.inputs.channel }} 61 | fi 62 | -------------------------------------------------------------------------------- /.github/workflows/publish-not-main.yml: -------------------------------------------------------------------------------- 1 | name: Publish (alpha / beta) 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | channel: 7 | type: choice 8 | description: NPM channel 9 | options: 10 | - alpha 11 | - beta 12 | 13 | permissions: 14 | contents: write 15 | 16 | jobs: 17 | publish-npm: 18 | runs-on: ubuntu-latest 19 | defaults: 20 | run: 21 | working-directory: ./ 22 | steps: 23 | - uses: actions/checkout@v2 24 | - uses: actions/setup-node@v1 25 | with: 26 | node-version: 16 27 | registry-url: https://registry.npmjs.org/ 28 | cache-dependency-path: ./package-lock.json 29 | 30 | - run: npx lerna@6 bootstrap 31 | - run: npx lerna@6 run build 32 | 33 | - name: Create release 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | run: | 37 | RELEASE_TAG=v$(node -p "require('./lerna.json').version") 38 | gh release create --prerelease $RELEASE_TAG --target=$GITHUB_SHA --title="$RELEASE_TAG" --generate-notes 39 | 40 | - name: Publish to npmjs 41 | env: 42 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 43 | run: npx lerna publish from-package --yes --dist-tag ${{ github.event.inputs.channel }} --pre-dist-tag ${{ github.event.inputs.channel }} 44 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | common: 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: ./ 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v1 18 | with: 19 | node-version: 16 20 | registry-url: https://registry.npmjs.org/ 21 | cache-dependency-path: ./package-lock.json 22 | 23 | - run: npx lerna@6 bootstrap 24 | - run: npx lerna@6 run build 25 | 26 | - name: Create release 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | run: | 30 | echo " - @multiversx/sdk-nestjs-common [npm](https://www.npmjs.com/package/@multiversx/sdk-nestjs-common)" >> notes.txt 31 | echo " - @multiversx/sdk-nestjs-auth [npm](https://www.npmjs.com/package/@multiversx/sdk-nestjs-auth)" >> notes.txt 32 | echo " - @multiversx/sdk-nestjs-http [npm](https://www.npmjs.com/package/@multiversx/sdk-nestjs-http)" >> notes.txt 33 | echo " - @multiversx/sdk-nestjs-monitoring [npm](https://www.npmjs.com/package/@multiversx/sdk-nestjs-monitoring)" >> notes.txt 34 | echo " - @multiversx/sdk-nestjs-elastic [npm](https://www.npmjs.com/package/@multiversx/sdk-nestjs-elastic)" >> notes.txt 35 | echo " - @multiversx/sdk-nestjs-redis [npm](https://www.npmjs.com/package/@multiversx/sdk-nestjs-redis)" >> notes.txt 36 | echo " - @multiversx/sdk-nestjs-rabbitmq [npm](https://www.npmjs.com/package/@multiversx/sdk-nestjs-rabbitmq)" >> notes.txt 37 | echo " - @multiversx/sdk-nestjs-cache [npm](https://www.npmjs.com/package/@multiversx/sdk-nestjs-cache)" >> notes.txt 38 | echo "" >> notes.txt 39 | 40 | RELEASE_TAG=v$(node -p "require('./lerna.json').version") 41 | gh release create $RELEASE_TAG --target=$GITHUB_SHA --title="$RELEASE_TAG" --generate-notes --notes-file=notes.txt 42 | 43 | - name: Publish to npmjs 44 | env: 45 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 46 | run: npx lerna@6 publish from-package --yes 47 | -------------------------------------------------------------------------------- /.github/workflows/unit.tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Unit tests 5 | 6 | on: 7 | push: 8 | 9 | jobs: 10 | common: 11 | runs-on: ubuntu-latest 12 | defaults: 13 | run: 14 | working-directory: . 15 | 16 | strategy: 17 | matrix: 18 | node-version: [20.x] 19 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: "npm" 28 | cache-dependency-path: ./package-lock.json 29 | - run: npx lerna@6 bootstrap 30 | - run: npx lerna@6 run build 31 | - run: npx lerna@6 run test 32 | env: 33 | CI: true 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | tsconfig.tsbuildinfo 4 | 5 | .DS_Store 6 | .idea/ 7 | .npmrc 8 | *.log 9 | *.tsbuildinfo -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | "eslint.validate": [ 6 | "javascript" 7 | ], 8 | "editor.formatOnSave": true, 9 | "files.exclude": { 10 | "**/.git": true, 11 | "**/.DS_Store": true, 12 | "**/node_modules": true, 13 | "**/coverage": true, 14 | "**/dist": true, 15 | "**/config/config.yaml": true, 16 | "**/lib": true, 17 | }, 18 | "search.exclude": { 19 | "**/node_modules": true, 20 | "**/coverage": true, 21 | "**/dist": true, 22 | }, 23 | "search.useIgnoreFiles": false 24 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-nestjs/6cda0cd7ef838245ac2d4d10280e4dd023ab922b/CHANGELOG.md -------------------------------------------------------------------------------- /eslintrc.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-nestjs/6cda0cd7ef838245ac2d4d10280e4dd023ab922b/eslintrc.json -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "js", 4 | "json", 5 | "ts" 6 | ], 7 | "rootDir": ".", 8 | "testRegex": ".*\\.spec\\.ts$", 9 | "transform": { 10 | "^.+\\.(t|j)s$": "ts-jest" 11 | }, 12 | "collectCoverageFrom": [ 13 | "**/*.(t|j)s" 14 | ], 15 | "coverageDirectory": "./coverage", 16 | "testEnvironment": "node", 17 | "roots": [ 18 | "/packages" 19 | ] 20 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "useWorkspaces": true, 4 | "version": "5.0.0", 5 | "packages": [ 6 | "packages/*" 7 | ], 8 | "npmClient": "npm" 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "devDependencies": { 8 | "@types/jest": "^29.5.1", 9 | "@types/js-yaml": "^4.0.9", 10 | "jest": "^29.7.0", 11 | "lerna": "^6.6.1", 12 | "ts-jest": "^29.1.1", 13 | "ts-node": "^10.9.1", 14 | "typescript": "^4.9.5" 15 | }, 16 | "dependencies": { 17 | "ajv": "^8.12.0", 18 | "js-yaml": "^4.1.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/auth/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | // 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | rules: { 18 | "@typescript-eslint/no-explicit-any": ["off"], 19 | "@typescript-eslint/no-unused-vars": ["off"], 20 | "@typescript-eslint/ban-ts-comment": ["off"], 21 | "@typescript-eslint/no-empty-function": ["off"], 22 | "@typescript-eslint/ban-types": ["off"], 23 | "@typescript-eslint/no-var-requires": ["off"], 24 | "@typescript-eslint/no-inferrable-types": ["off"], 25 | "require-await": ["error"], 26 | "@typescript-eslint/no-floating-promises": ["error"], 27 | "max-len": ["off"], 28 | "semi": ["error"], 29 | "comma-dangle": ["error", "always-multiline"], 30 | "eol-last": ["error"], 31 | }, 32 | ignorePatterns: ['.eslintrc.js', '*.spec.ts'], 33 | }; -------------------------------------------------------------------------------- /packages/auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@multiversx/sdk-nestjs-auth", 3 | "version": "5.0.0", 4 | "description": "Multiversx SDK Nestjs auth package", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/**/*" 9 | ], 10 | "scripts": { 11 | "test": "jest --config=../../jest.config.json --passWithNoTests auth/*", 12 | "build": "tsc", 13 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", 14 | "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/multiversx/mx-sdk-nestjs.git" 19 | }, 20 | "keywords": [ 21 | "multiversx", 22 | "nestjs", 23 | "auth" 24 | ], 25 | "author": "MultiversX", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@types/jsonwebtoken": "^9.0.1", 29 | "@typescript-eslint/eslint-plugin": "^5.12.0", 30 | "@typescript-eslint/parser": "^5.16.0", 31 | "eslint": "^8.9.0", 32 | "typescript": "^4.3.5" 33 | }, 34 | "dependencies": { 35 | "@multiversx/sdk-core": "^14.0.0", 36 | "@multiversx/sdk-native-auth-server": "^2.0.0", 37 | "jsonwebtoken": "^9.0.0" 38 | }, 39 | "peerDependencies": { 40 | "@multiversx/sdk-nestjs-cache": "^5.0.0", 41 | "@multiversx/sdk-nestjs-common": "^5.0.0", 42 | "@multiversx/sdk-nestjs-monitoring": "^5.0.0", 43 | "@nestjs/common": "^10.x" 44 | }, 45 | "publishConfig": { 46 | "access": "public" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/auth/src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './native.auth'; 2 | export * from './no.auth'; 3 | export * from './jwt'; 4 | -------------------------------------------------------------------------------- /packages/auth/src/decorators/jwt.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from "@nestjs/common"; 2 | 3 | export const Jwt = createParamDecorator((field, req) => { 4 | const jwt = req.args[0].jwt; 5 | 6 | if (jwt && field) { 7 | const fieldsChain = field.split('.'); 8 | const data = fieldsChain.reduce((value: any, field: string) => value ? value[field] : undefined, jwt); 9 | return data; 10 | } 11 | 12 | return jwt; 13 | }); 14 | -------------------------------------------------------------------------------- /packages/auth/src/decorators/native.auth.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | 3 | export const NativeAuth = createParamDecorator((key, req) => { 4 | const nativeAuth = req.args[0].nativeAuth; 5 | if (!nativeAuth) { 6 | return undefined; 7 | } 8 | 9 | if (key === undefined) { 10 | return nativeAuth; 11 | } 12 | 13 | return nativeAuth[key]; 14 | }); 15 | -------------------------------------------------------------------------------- /packages/auth/src/decorators/no.auth.ts: -------------------------------------------------------------------------------- 1 | import { DecoratorUtils } from "@multiversx/sdk-nestjs-common/lib/utils/decorator.utils"; 2 | 3 | export class NoAuthOptions { } 4 | 5 | export const NoAuth = DecoratorUtils.registerMethodDecorator(NoAuthOptions); 6 | -------------------------------------------------------------------------------- /packages/auth/src/errors/native.auth.invalid.origin.error.ts: -------------------------------------------------------------------------------- 1 | import { NativeAuthError } from "@multiversx/sdk-native-auth-server"; 2 | 3 | export class NativeAuthInvalidOriginError extends NativeAuthError { 4 | constructor(actualOrigin: string, expectedOrigin: string) { 5 | super(`Invalid origin '${actualOrigin}'. should be '${expectedOrigin}'`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/auth/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './errors/native.auth.invalid.origin.error'; 2 | export * from './jwt.admin.guard'; 3 | export * from './jwt.or.native.auth.guard'; 4 | export * from './jwt.authenticate.guard'; 5 | export * from './native.auth.admin.guard'; 6 | export * from './native.auth.guard'; 7 | export * from './decorators'; 8 | -------------------------------------------------------------------------------- /packages/auth/src/jwt.admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext, Inject } from '@nestjs/common'; 2 | import { ExecutionContextUtils, MxnestConfigService, MXNEST_CONFIG_SERVICE } from '@multiversx/sdk-nestjs-common'; 3 | 4 | @Injectable() 5 | export class JwtAdminGuard implements CanActivate { 6 | constructor( 7 | @Inject(MXNEST_CONFIG_SERVICE) 8 | private readonly mxnestConfigService: MxnestConfigService 9 | ) { } 10 | 11 | // eslint-disable-next-line require-await 12 | async canActivate( 13 | context: ExecutionContext, 14 | ): Promise { 15 | 16 | 17 | const admins = this.mxnestConfigService.getSecurityAdmins(); 18 | if (!admins) { 19 | return false; 20 | } 21 | 22 | const request = ExecutionContextUtils.getRequest(context); 23 | 24 | return admins.includes(request.jwt.address); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/auth/src/jwt.authenticate.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext, Inject } from '@nestjs/common'; 2 | import { verify } from 'jsonwebtoken'; 3 | import { PerformanceProfiler } from '@multiversx/sdk-nestjs-monitoring'; 4 | import { MxnestConfigService, MXNEST_CONFIG_SERVICE, DecoratorUtils, ExecutionContextUtils } from '@multiversx/sdk-nestjs-common'; 5 | import { NoAuthOptions } from './decorators/no.auth'; 6 | 7 | @Injectable() 8 | export class JwtAuthenticateGuard implements CanActivate { 9 | constructor( 10 | @Inject(MXNEST_CONFIG_SERVICE) 11 | private readonly mxnestConfigService: MxnestConfigService 12 | ) { } 13 | 14 | async canActivate( 15 | context: ExecutionContext, 16 | ): Promise { 17 | const noAuthMetadata = DecoratorUtils.getMethodDecorator(NoAuthOptions, context.getHandler()); 18 | if (noAuthMetadata) { 19 | return true; 20 | } 21 | 22 | const headers = ExecutionContextUtils.getHeaders(context); 23 | const request = ExecutionContextUtils.getRequest(context); 24 | 25 | const authorization: string = headers['authorization']; 26 | if (!authorization) { 27 | return false; 28 | } 29 | 30 | const jwt = authorization.replace('Bearer ', ''); 31 | const profiler = new PerformanceProfiler(); 32 | 33 | try { 34 | const jwtSecret = this.mxnestConfigService.getJwtSecret(); 35 | 36 | request.jwt = await new Promise((resolve, reject) => { 37 | verify(jwt, jwtSecret, (err: any, decoded: any) => { 38 | if (err) { 39 | reject(err); 40 | } 41 | 42 | // @ts-ignore 43 | resolve({ 44 | ...decoded.user, 45 | ...decoded, 46 | }); 47 | }); 48 | }); 49 | 50 | } catch (error) { 51 | // @ts-ignore 52 | const message = error?.message; 53 | if (message) { 54 | profiler.stop(); 55 | 56 | request.res.set('X-Jwt-Auth-Error-Type', error.constructor.name); 57 | request.res.set('X-Jwt-Auth-Error-Message', message); 58 | request.res.set('X-Jwt-Auth-Duration', profiler.duration); 59 | } 60 | 61 | return false; 62 | } 63 | 64 | return true; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/auth/src/jwt.or.native.auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext, Inject, Optional } from '@nestjs/common'; 2 | import { CacheService } from '@multiversx/sdk-nestjs-cache'; 3 | import { MxnestConfigService, MXNEST_CONFIG_SERVICE } from '@multiversx/sdk-nestjs-common'; 4 | import { JwtAuthenticateGuard } from './jwt.authenticate.guard'; 5 | import { NativeAuthGuard } from './native.auth.guard'; 6 | 7 | @Injectable() 8 | export class JwtOrNativeAuthGuard implements CanActivate { 9 | constructor( 10 | @Inject(MXNEST_CONFIG_SERVICE) private readonly mxnestConfigService: MxnestConfigService, 11 | @Optional() private readonly cacheService?: CacheService, 12 | ) { } 13 | 14 | async canActivate(context: ExecutionContext): Promise { 15 | const jwtGuard = new JwtAuthenticateGuard(this.mxnestConfigService); 16 | const nativeAuthGuard = new NativeAuthGuard(this.mxnestConfigService, this.cacheService); 17 | 18 | try { 19 | const result = await jwtGuard.canActivate(context); 20 | if (result) { 21 | return true; 22 | } 23 | } catch (error) { 24 | // do nothing 25 | } 26 | 27 | try { 28 | return await nativeAuthGuard.canActivate(context); 29 | } catch (error) { 30 | return false; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/auth/src/native.auth.admin.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext, Inject } from '@nestjs/common'; 2 | import { ExecutionContextUtils, MxnestConfigService, MXNEST_CONFIG_SERVICE } from '@multiversx/sdk-nestjs-common'; 3 | 4 | /** 5 | * This Guard allows only specific addresses to be authenticated. 6 | * 7 | * The addresses are defined in the config file and are passed to the guard via the MxnestConfigService. 8 | * 9 | * @return {boolean} `canActivate` returns true if the address is in the list of admins and uses a valid Native-Auth token. 10 | * 11 | * @param {CachingService} CachingService - Dependency of `NativeAuthGuard` 12 | * @param {MxnestConfigService} MxnestConfigService - Dependency of `NativeAuthGuard`. Also used to get the list of admins (`getSecurityAdmins`). 13 | */ 14 | @Injectable() 15 | export class NativeAuthAdminGuard implements CanActivate { 16 | constructor( 17 | @Inject(MXNEST_CONFIG_SERVICE) 18 | private readonly mxnestConfigService: MxnestConfigService 19 | ) { } 20 | 21 | canActivate(context: ExecutionContext): boolean { 22 | const admins = this.mxnestConfigService.getSecurityAdmins(); 23 | if (!admins) { 24 | return false; 25 | } 26 | 27 | const request = ExecutionContextUtils.getRequest(context); 28 | 29 | return admins.includes(request.nativeAuth.signerAddress); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/auth/test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-nestjs/6cda0cd7ef838245ac2d4d10280e4dd023ab922b/packages/auth/test/.gitkeep -------------------------------------------------------------------------------- /packages/auth/test/native.auth.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { NativeAuthGuard } from "../src/native.auth.guard"; 2 | 3 | describe('getOrigin', () => { 4 | it('origin tests', () => { 5 | expect(NativeAuthGuard.getOrigin({ origin: 'https://localhost:3001' })).toStrictEqual('https://localhost:3001'); 6 | expect(NativeAuthGuard.getOrigin({ origin: 'https://api.multiversx.com' })).toStrictEqual('https://api.multiversx.com'); 7 | expect(NativeAuthGuard.getOrigin({ origin: 'http://localhost' })).toStrictEqual('http://localhost'); 8 | }); 9 | 10 | it('referer tests', () => { 11 | expect(NativeAuthGuard.getOrigin({ referer: 'https://localhost:3001/' })).toStrictEqual('https://localhost:3001'); 12 | expect(NativeAuthGuard.getOrigin({ referer: 'http://localhost:3001/helloworld' })).toStrictEqual('http://localhost:3001'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Import non-ES modules as default imports. 4 | "outDir": "./lib", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "es2017", 12 | "sourceMap": true, 13 | "baseUrl": "./", 14 | "incremental": true, 15 | // enable strict null checks as a best practice 16 | "strictNullChecks": true, 17 | // Search under node_modules for non-relative imports. 18 | "moduleResolution": "node", 19 | // Process & infer types from .js files. 20 | "allowJs": true, 21 | // Enable strictest settings like strictNullChecks & noImplicitAny. 22 | "strict": true, 23 | // Import non-ES modules as default imports. 24 | "esModuleInterop": true, 25 | "alwaysStrict": true, 26 | "allowUnreachableCode": false, 27 | "noImplicitAny": true, 28 | "noImplicitThis": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noImplicitReturns": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "forceConsistentCasingInFileNames": true, 34 | "importHelpers": true, 35 | }, 36 | "include": [ 37 | "src" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } -------------------------------------------------------------------------------- /packages/cache/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | // 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | rules: { 18 | "@typescript-eslint/no-explicit-any": ["off"], 19 | "@typescript-eslint/no-unused-vars": ["off"], 20 | "@typescript-eslint/ban-ts-comment": ["off"], 21 | "@typescript-eslint/no-empty-function": ["off"], 22 | "@typescript-eslint/ban-types": ["off"], 23 | "@typescript-eslint/no-var-requires": ["off"], 24 | "@typescript-eslint/no-inferrable-types": ["off"], 25 | "require-await": ["error"], 26 | "@typescript-eslint/no-floating-promises": ["error"], 27 | "max-len": ["off"], 28 | "semi": ["error"], 29 | "comma-dangle": ["error", "always-multiline"], 30 | "eol-last": ["error"], 31 | }, 32 | ignorePatterns: ['.eslintrc.js', '*.spec.ts'], 33 | }; -------------------------------------------------------------------------------- /packages/cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@multiversx/sdk-nestjs-cache", 3 | "version": "5.0.0", 4 | "description": "Multiversx SDK Nestjs cache package", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/**/*" 9 | ], 10 | "scripts": { 11 | "test": "jest --config=../../jest.config.json --passWithNoTests cache/*", 12 | "build": "tsc", 13 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", 14 | "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/multiversx/mx-sdk-nestjs.git" 19 | }, 20 | "keywords": [ 21 | "multiversx", 22 | "nestjs", 23 | "cache" 24 | ], 25 | "author": "MultiversX", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@types/node": "^16.11.10", 29 | "@types/tiny-async-pool": "^1.0.0", 30 | "@typescript-eslint/eslint-plugin": "^5.12.0", 31 | "@typescript-eslint/parser": "^5.16.0", 32 | "eslint": "^8.9.0", 33 | "typescript": "^4.3.5" 34 | }, 35 | "dependencies": { 36 | "lru-cache": "^8.0.4", 37 | "moment": "^2.29.4", 38 | "redis": "^3.1.2", 39 | "tiny-async-pool": "^1.2.0", 40 | "uuid": "^8.3.2" 41 | }, 42 | "peerDependencies": { 43 | "@multiversx/sdk-nestjs-common": "^5.0.0", 44 | "@multiversx/sdk-nestjs-monitoring": "^5.0.0", 45 | "@multiversx/sdk-nestjs-redis": "^5.0.0", 46 | "@nestjs/common": "^10.x", 47 | "@nestjs/core": "^10.x" 48 | }, 49 | "publishConfig": { 50 | "access": "public" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/cache/src/cache/cache.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Module } from '@nestjs/common'; 2 | import { CacheService } from './cache.service'; 3 | import { InMemoryCacheModule } from '../in-memory-cache/in-memory-cache.module'; 4 | import { RedisCacheModule } from '../redis-cache/redis-cache.module'; 5 | import { RedisCacheModuleAsyncOptions, RedisCacheModuleOptions } from '../redis-cache/options'; 6 | import { InMemoryCacheOptions } from '../in-memory-cache/entities/in-memory-cache-options.interface'; 7 | import { ADDITIONAL_CACHING_OPTIONS } from '../entities/common'; 8 | 9 | @Global() 10 | @Module({}) 11 | export class CacheModule { 12 | static forRoot( 13 | redisCacheModuleOptions: RedisCacheModuleOptions, 14 | inMemoryCacheModuleOptions?: InMemoryCacheOptions 15 | ): DynamicModule { 16 | return { 17 | module: CacheModule, 18 | imports: [ 19 | InMemoryCacheModule.forRoot(inMemoryCacheModuleOptions), 20 | RedisCacheModule.forRoot(redisCacheModuleOptions), 21 | ], 22 | providers: [ 23 | { 24 | provide: ADDITIONAL_CACHING_OPTIONS, 25 | useValue: redisCacheModuleOptions.additionalOptions, 26 | }, 27 | CacheService, 28 | ], 29 | exports: [ 30 | CacheService, 31 | ], 32 | }; 33 | } 34 | 35 | static forRootAsync( 36 | redisCacheModuleAsyncOptions: RedisCacheModuleAsyncOptions, 37 | inMemoryCacheModuleOptions?: InMemoryCacheOptions 38 | ): DynamicModule { 39 | return { 40 | module: CacheModule, 41 | imports: [ 42 | InMemoryCacheModule.forRoot(inMemoryCacheModuleOptions), 43 | RedisCacheModule.forRootAsync(redisCacheModuleAsyncOptions), 44 | ...(redisCacheModuleAsyncOptions.imports || []), 45 | ], 46 | providers: [ 47 | { 48 | provide: ADDITIONAL_CACHING_OPTIONS, 49 | useFactory: async (...args: any[]) => { 50 | const factoryData = await redisCacheModuleAsyncOptions.useFactory(...args); 51 | return factoryData.additionalOptions; 52 | }, 53 | inject: redisCacheModuleAsyncOptions.inject || [], 54 | }, 55 | CacheService, 56 | ], 57 | exports: [ 58 | CacheService, 59 | ], 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/cache/src/cache/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cache.module'; 2 | export * from './cache.service'; 3 | -------------------------------------------------------------------------------- /packages/cache/src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './no.cache'; 2 | -------------------------------------------------------------------------------- /packages/cache/src/decorators/no.cache.ts: -------------------------------------------------------------------------------- 1 | import { DecoratorUtils } from "@multiversx/sdk-nestjs-common/lib/utils/decorator.utils"; 2 | 3 | export class NoCacheOptions { } 4 | 5 | export const NoCache = DecoratorUtils.registerMethodDecorator(NoCacheOptions); 6 | -------------------------------------------------------------------------------- /packages/cache/src/entities/caching.module.async.options.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata } from "@nestjs/common"; 2 | import { CachingModuleOptions } from "./caching.module.options"; 3 | 4 | export interface CachingModuleAsyncOptions extends Pick { 5 | useFactory: (...args: any[]) => Promise | CachingModuleOptions; 6 | inject?: any[]; 7 | } 8 | -------------------------------------------------------------------------------- /packages/cache/src/entities/caching.module.options.ts: -------------------------------------------------------------------------------- 1 | export class CachingModuleOptions { 2 | constructor(init?: Partial) { 3 | Object.assign(this, init); 4 | } 5 | 6 | url: string = ''; 7 | 8 | port: number = 6379; 9 | 10 | password: string | undefined; 11 | 12 | poolLimit: number = 100; 13 | 14 | processTtl: number = 60; 15 | } 16 | -------------------------------------------------------------------------------- /packages/cache/src/entities/common.ts: -------------------------------------------------------------------------------- 1 | export const ADDITIONAL_CACHING_OPTIONS = 'ADDITIONAL_CACHING_OPTIONS'; 2 | -------------------------------------------------------------------------------- /packages/cache/src/entities/guest.caching.ts: -------------------------------------------------------------------------------- 1 | export enum GuestCacheMethodEnum { 2 | GET = 'GET', 3 | POST = 'POST' 4 | } 5 | 6 | export interface IGuestCacheEntity { 7 | method: GuestCacheMethodEnum; 8 | body?: any, 9 | path: string; 10 | } 11 | 12 | export interface IGuestCacheWarmerOptions { 13 | cacheTtl?: number, 14 | targetUrl: string, 15 | cacheTriggerHitsThreshold?: number; 16 | } 17 | 18 | export interface IGuestCacheOptions { 19 | batchSize?: number; 20 | ignoreAuthorizationHeader?: boolean; 21 | } 22 | 23 | export const DATE_FORMAT = "YYYY-MM-DD_HH:mm"; 24 | export const REDIS_PREFIX = "guestCache"; 25 | -------------------------------------------------------------------------------- /packages/cache/src/entities/local.cache.value.ts: -------------------------------------------------------------------------------- 1 | export class LocalCacheValue { 2 | value: any; 3 | 4 | expires: number = 0; 5 | } 6 | -------------------------------------------------------------------------------- /packages/cache/src/guest-cache/guest-cache.middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | NestMiddleware, 4 | mixin, 5 | Type, 6 | } from '@nestjs/common'; 7 | import { IGuestCacheOptions } from '../entities/guest.caching'; 8 | import { GuestCacheService } from './guest-cache.service'; 9 | 10 | export const GuestCacheMiddlewareCreator = (options?: IGuestCacheOptions): Type => { 11 | @Injectable() 12 | class GuestCacheMiddleware implements NestMiddleware { 13 | 14 | constructor(private guestCaching: GuestCacheService) { } 15 | 16 | async use(req: any, res: any, next: any) { 17 | const cacheResult = await this.guestCaching.getOrSetRequestCache(req, options); 18 | 19 | if (cacheResult.fromCache) { 20 | res.setHeader('X-Guest-Cache-Hit', cacheResult.fromCache); 21 | return res.json(cacheResult.response); 22 | } 23 | 24 | return next(); 25 | } 26 | } 27 | 28 | return mixin(GuestCacheMiddleware); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/cache/src/guest-cache/index.ts: -------------------------------------------------------------------------------- 1 | export * from './guest-cache.warmer'; 2 | export * from './guest-cache.middleware'; 3 | export * from './guest-cache.service'; 4 | -------------------------------------------------------------------------------- /packages/cache/src/in-memory-cache/entities/common.constants.ts: -------------------------------------------------------------------------------- 1 | export const LRU_CACHE_MAX_ITEMS = 20000; 2 | export const IN_MEMORY_CACHE_OPTIONS = 'IN_MEMORY_CACHE_OPTIONS'; 3 | -------------------------------------------------------------------------------- /packages/cache/src/in-memory-cache/entities/in-memory-cache-options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface InMemoryCacheOptions { 2 | maxItems?: number; 3 | skipItemsSerialization?: boolean; 4 | } 5 | -------------------------------------------------------------------------------- /packages/cache/src/in-memory-cache/in-memory-cache.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { IN_MEMORY_CACHE_OPTIONS } from './entities/common.constants'; 3 | import { InMemoryCacheOptions } from './entities/in-memory-cache-options.interface'; 4 | import { InMemoryCacheService } from './in-memory-cache.service'; 5 | 6 | @Module({}) 7 | export class InMemoryCacheModule { 8 | public static forRoot(inMemoryCacheOptions?: InMemoryCacheOptions): DynamicModule { 9 | return { 10 | module: InMemoryCacheModule, 11 | providers: [ 12 | { 13 | provide: IN_MEMORY_CACHE_OPTIONS, 14 | useValue: inMemoryCacheOptions, 15 | }, 16 | InMemoryCacheService, 17 | ], 18 | exports: [InMemoryCacheService], 19 | }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/cache/src/in-memory-cache/index.ts: -------------------------------------------------------------------------------- 1 | export * from './in-memory-cache.module'; 2 | export * from './in-memory-cache.service'; 3 | -------------------------------------------------------------------------------- /packages/cache/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './redis-cache'; 2 | export * from './redlock'; 3 | export * from './in-memory-cache'; 4 | export * from './cache'; 5 | export * from './jitter'; 6 | export * from './interceptors/caching.interceptor'; 7 | export * from './interceptors/guest.cache.interceptor'; 8 | export * from './guest-cache'; 9 | export * from './decorators'; 10 | -------------------------------------------------------------------------------- /packages/cache/src/interceptors/guest.cache.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; 2 | import { Observable, of } from "rxjs"; 3 | import { DecoratorUtils } from "@multiversx/sdk-nestjs-common"; 4 | import { IGuestCacheOptions } from "../entities/guest.caching"; 5 | import { GuestCacheService } from "../guest-cache/guest-cache.service"; 6 | import { NoCacheOptions } from "../decorators"; 7 | 8 | @Injectable() 9 | export class GuestCacheInterceptor implements NestInterceptor { 10 | private guestCacheOptions; 11 | 12 | constructor( 13 | private readonly guestCacheService: GuestCacheService, 14 | guestCacheOptions?: IGuestCacheOptions 15 | ) { 16 | this.guestCacheOptions = guestCacheOptions; 17 | } 18 | 19 | async intercept(context: ExecutionContext, next: CallHandler): Promise> { 20 | const contextType: string = context.getType(); 21 | 22 | if (!["http", "https"].includes(contextType)) { 23 | return next.handle(); 24 | } 25 | 26 | const request = context.getArgByIndex(0); 27 | if (request.method !== 'GET') { 28 | return next.handle(); 29 | } 30 | 31 | const cachingMetadata = DecoratorUtils.getMethodDecorator(NoCacheOptions, context.getHandler()); 32 | if (cachingMetadata) { 33 | return next.handle(); 34 | } 35 | 36 | const cacheResult = await this.guestCacheService.getOrSetRequestCache(request, this.guestCacheOptions); 37 | if (cacheResult.fromCache) { 38 | return of(cacheResult.response); 39 | } 40 | 41 | return next.handle(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/cache/src/jitter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @param ttlSeconds Time to live seconds 3 | * @param slidingPercentage sliding percentage for example 10% is 0.1 4 | * @returns jitter in seconds. 5 | */ 6 | export const jitter = ( 7 | ttlSeconds: number, 8 | slidingPercentage = 0.1, 9 | ): number => { 10 | if (!ttlSeconds) { 11 | throw Error('ttl is required'); 12 | } 13 | const ttl = Number(ttlSeconds); 14 | if (Number.isNaN(ttl)) { 15 | throw Error(`error occur while trying to apply jitter, ${ttlSeconds} is NaN!`); 16 | } 17 | const slidingExpirationFactor = (Math.random() * slidingPercentage * 2) - slidingPercentage; // (0 to 1) * 0.2 - 0.1 --> +-10% 18 | return ttl + Math.round(ttl * slidingExpirationFactor); // ttl + jitter sliding in second 19 | }; 20 | -------------------------------------------------------------------------------- /packages/cache/src/local.cache.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { PerformanceProfiler } from "@multiversx/sdk-nestjs-monitoring"; 3 | import { LocalCacheValue } from "./entities/local.cache.value"; 4 | 5 | @Injectable() 6 | export class LocalCacheService { 7 | private static readonly dictionary: { [key: string]: LocalCacheValue } = {}; 8 | 9 | private static lastPruneTime: number = new Date().getTime(); 10 | 11 | setCacheValue(key: string, value: T, ttl: number): T { 12 | if (this.needsPrune()) { 13 | this.prune(); 14 | } 15 | 16 | const expires = new Date().getTime() + (ttl * 1000); 17 | 18 | LocalCacheService.dictionary[key] = { 19 | value, 20 | expires, 21 | }; 22 | 23 | return value; 24 | } 25 | 26 | getCacheValue(key: string): T | undefined { 27 | const cacheValue = LocalCacheService.dictionary[key]; 28 | if (!cacheValue) { 29 | return undefined; 30 | } 31 | 32 | const now = new Date().getTime(); 33 | if (cacheValue.expires < now) { 34 | delete LocalCacheService.dictionary[key]; 35 | return undefined; 36 | } 37 | 38 | return cacheValue.value; 39 | } 40 | 41 | deleteCacheKey(key: string) { 42 | delete LocalCacheService.dictionary[key]; 43 | } 44 | 45 | needsPrune() { 46 | return new Date().getTime() > LocalCacheService.lastPruneTime + 60000; 47 | } 48 | 49 | prune() { 50 | const now = new Date().getTime(); 51 | LocalCacheService.lastPruneTime = now; 52 | 53 | const profiler = new PerformanceProfiler(); 54 | 55 | const keys = Object.keys(LocalCacheService.dictionary); 56 | 57 | for (const key of keys) { 58 | const value = LocalCacheService.dictionary[key]; 59 | if (value.expires < now) { 60 | delete LocalCacheService.dictionary[key]; 61 | } 62 | } 63 | 64 | const keysAfter = Object.keys(LocalCacheService.dictionary); 65 | 66 | profiler.stop(`Local cache prune. Deleted ${keys.length - keysAfter.length} keys. Total keys in cache: ${keysAfter.length}`, true); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/cache/src/redis-cache/index.ts: -------------------------------------------------------------------------------- 1 | export * from './redis-cache.module'; 2 | export * from './redis-cache.service'; 3 | export * from './options'; 4 | -------------------------------------------------------------------------------- /packages/cache/src/redis-cache/options.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata } from '@nestjs/common'; 2 | import { ConnectionOptions } from 'tls'; 3 | 4 | export class RedisCacheModuleOptions { 5 | config: { 6 | host?: string | undefined; 7 | port?: number | undefined; 8 | username?: string | undefined; 9 | password?: string | undefined; 10 | sentinelUsername?: string | undefined; 11 | sentinelPassword?: string | undefined; 12 | sentinels?: Array<{ host: string; port: number }> | undefined; 13 | connectTimeout?: number | undefined; 14 | name?: string | undefined; 15 | tls?: ConnectionOptions | undefined; 16 | db?: number | undefined; 17 | enableAutoPipelining?: boolean | undefined; 18 | autoPipeliningIgnoredCommands?: string[] | undefined; 19 | }; 20 | 21 | additionalOptions?: { 22 | poolLimit?: number | undefined; 23 | processTtl?: number | undefined; 24 | }; 25 | 26 | constructor( 27 | options: RedisCacheModuleOptions['config'], 28 | additionalOptions?: RedisCacheModuleOptions['additionalOptions'], 29 | ) { 30 | this.config = {}; 31 | this.additionalOptions = {}; 32 | Object.assign(this.config, options); 33 | Object.assign(this.additionalOptions, additionalOptions); 34 | } 35 | } 36 | 37 | export interface RedisCacheModuleAsyncOptions extends Pick { 38 | useFactory: (...args: any[]) => Promise | RedisCacheModuleOptions; 39 | inject?: any[]; 40 | } 41 | -------------------------------------------------------------------------------- /packages/cache/src/redis-cache/redis-cache.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { RedisCacheService } from './redis-cache.service'; 3 | import { RedisCacheModuleOptions, RedisCacheModuleAsyncOptions } from './options'; 4 | import { MetricsModule } from '@multiversx/sdk-nestjs-monitoring'; 5 | import { RedisModule } from '@multiversx/sdk-nestjs-redis'; 6 | 7 | @Module({}) 8 | export class RedisCacheModule { 9 | static forRoot(options: RedisCacheModuleOptions): DynamicModule { 10 | return { 11 | module: RedisCacheModule, 12 | imports: [ 13 | RedisModule.forRoot(options), 14 | MetricsModule, 15 | ], 16 | providers: [ 17 | RedisCacheService, 18 | ], 19 | exports: [ 20 | RedisCacheService, 21 | ], 22 | }; 23 | } 24 | 25 | static forRootAsync(asyncOptions: RedisCacheModuleAsyncOptions): DynamicModule { 26 | return { 27 | module: RedisCacheModule, 28 | imports: [ 29 | RedisModule.forRootAsync(asyncOptions), 30 | MetricsModule, 31 | ], 32 | providers: [ 33 | RedisCacheService, 34 | ], 35 | exports: [ 36 | RedisCacheService, 37 | ], 38 | }; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/cache/src/redlock/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './redlock.connection.options'; 2 | export * from './redlock.connection.async.options'; 3 | -------------------------------------------------------------------------------- /packages/cache/src/redlock/entities/redlock.connection.async.options.ts: -------------------------------------------------------------------------------- 1 | import { RedlockConnectionOptions } from './redlock.connection.options'; 2 | import { ModuleMetadata } from '@nestjs/common'; 3 | 4 | export interface RedlockConnectionAsyncOptions extends Pick { 5 | useFactory: (...args: any[]) => Promise | RedlockConnectionOptions[]; 6 | inject?: any[]; 7 | imports?: any[]; 8 | } 9 | -------------------------------------------------------------------------------- /packages/cache/src/redlock/entities/redlock.connection.options.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions } from 'tls'; 2 | 3 | export class RedlockConnectionOptions { 4 | host?: string | undefined; 5 | port?: number | undefined; 6 | username?: string | undefined; 7 | password?: string | undefined; 8 | sentinelUsername?: string | undefined; 9 | sentinelPassword?: string | undefined; 10 | sentinels?: Array<{ host: string; port: number }> | undefined; 11 | connectTimeout?: number | undefined; 12 | name?: string | undefined; 13 | tls?: ConnectionOptions | undefined; 14 | db?: number | undefined; 15 | 16 | constructor(init?: Partial) { 17 | Object.assign(this, init); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/cache/src/redlock/entities/redlock.log.level.ts: -------------------------------------------------------------------------------- 1 | export enum RedlockLogLevel { 2 | NONE = 'none', 3 | WARNING = 'warning', 4 | ERROR = 'error', 5 | } 6 | -------------------------------------------------------------------------------- /packages/cache/src/redlock/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lock.timeout.error'; 2 | -------------------------------------------------------------------------------- /packages/cache/src/redlock/errors/lock.timeout.error.ts: -------------------------------------------------------------------------------- 1 | export class LockTimeoutError extends Error { 2 | constructor(lockKey: string) { 3 | super(`Timed out while attempting to acquire lock for resource '${lockKey}'`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/cache/src/redlock/index.ts: -------------------------------------------------------------------------------- 1 | export * from './redlock.configuration'; 2 | export * from './redlock.constants'; 3 | export * from './redlock.module'; 4 | export * from './redlock.service'; 5 | export * from './entities'; 6 | export * from './errors'; 7 | -------------------------------------------------------------------------------- /packages/cache/src/redlock/redlock.configuration.ts: -------------------------------------------------------------------------------- 1 | export interface RedlockConfiguration { 2 | keyExpiration: number; 3 | maxRetries: number; 4 | retryInterval: number; 5 | extendTtl?: number; 6 | } 7 | -------------------------------------------------------------------------------- /packages/cache/src/redlock/redlock.constants.ts: -------------------------------------------------------------------------------- 1 | export declare const REDLOCK_TOKEN = "REDLOCK_TOKEN"; 2 | -------------------------------------------------------------------------------- /packages/cache/src/redlock/redlock.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module, Provider } from '@nestjs/common'; 2 | import { MetricsModule } from '@multiversx/sdk-nestjs-monitoring'; 3 | import { RedisModule } from '@multiversx/sdk-nestjs-redis'; 4 | import { RedlockService } from './redlock.service'; 5 | import { RedlockConnectionAsyncOptions, RedlockConnectionOptions } from './entities'; 6 | import Redis from 'ioredis'; 7 | 8 | @Module({}) 9 | export class RedlockModule { 10 | static forRoot(...redisOptionsArray: RedlockConnectionOptions[]): DynamicModule { 11 | const redisProviders: Provider[] = redisOptionsArray.map((option, index) => ({ 12 | provide: `REDIS_CLIENT_${index}`, 13 | useFactory: () => new Redis(option), 14 | })); 15 | 16 | const redisClientsProvider: Provider = { 17 | provide: 'REDIS_CLIENTS', 18 | useFactory: (...clients: Redis[]) => clients, 19 | inject: redisProviders.map((_, index) => `REDIS_CLIENT_${index}`), 20 | }; 21 | 22 | return { 23 | module: RedlockModule, 24 | imports: [ 25 | RedisModule, // Import RedisModule normally 26 | MetricsModule, 27 | ], 28 | providers: [ 29 | ...redisProviders, 30 | redisClientsProvider, 31 | RedlockService, 32 | ], 33 | exports: [ 34 | 'REDIS_CLIENTS', 35 | RedlockService, 36 | ], 37 | }; 38 | } 39 | 40 | static forRootAsync(asyncOptions: RedlockConnectionAsyncOptions): DynamicModule { 41 | const asyncProviders: Provider[] = [{ 42 | provide: 'REDIS_CLIENTS', 43 | useFactory: async (...args: any[]): Promise => { 44 | const optionsArray = await asyncOptions.useFactory(...args); 45 | return optionsArray.map(options => new Redis(options)); 46 | }, 47 | inject: asyncOptions.inject || [], 48 | }]; 49 | 50 | return { 51 | module: RedlockModule, 52 | imports: [ 53 | RedisModule, 54 | MetricsModule, 55 | ...(asyncOptions.imports || []), 56 | ], 57 | providers: [ 58 | ...asyncProviders, 59 | RedlockService, 60 | ], 61 | exports: [ 62 | 'REDIS_CLIENTS', 63 | RedlockService, 64 | ], 65 | }; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /packages/cache/test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-nestjs/6cda0cd7ef838245ac2d4d10280e4dd023ab922b/packages/cache/test/.gitkeep -------------------------------------------------------------------------------- /packages/cache/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Import non-ES modules as default imports. 4 | "outDir": "./lib", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "es2017", 12 | "sourceMap": true, 13 | "baseUrl": "./", 14 | "incremental": true, 15 | // enable strict null checks as a best practice 16 | "strictNullChecks": true, 17 | // Search under node_modules for non-relative imports. 18 | "moduleResolution": "node", 19 | // Process & infer types from .js files. 20 | "allowJs": true, 21 | // Enable strictest settings like strictNullChecks & noImplicitAny. 22 | "strict": true, 23 | // Import non-ES modules as default imports. 24 | "esModuleInterop": true, 25 | "alwaysStrict": true, 26 | "allowUnreachableCode": false, 27 | "noImplicitAny": true, 28 | "noImplicitThis": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noImplicitReturns": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "forceConsistentCasingInFileNames": true, 34 | "importHelpers": true, 35 | }, 36 | "include": [ 37 | "src" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } -------------------------------------------------------------------------------- /packages/common/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | // 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | rules: { 18 | "@typescript-eslint/no-explicit-any": ["off"], 19 | "@typescript-eslint/no-unused-vars": ["off"], 20 | "@typescript-eslint/ban-ts-comment": ["off"], 21 | "@typescript-eslint/no-empty-function": ["off"], 22 | "@typescript-eslint/ban-types": ["off"], 23 | "@typescript-eslint/no-var-requires": ["off"], 24 | "@typescript-eslint/no-inferrable-types": ["off"], 25 | "require-await": ["error"], 26 | "@typescript-eslint/no-floating-promises": ["error"], 27 | "max-len": ["off"], 28 | "semi": ["error"], 29 | "comma-dangle": ["error", "always-multiline"], 30 | "eol-last": ["error"], 31 | }, 32 | ignorePatterns: ['.eslintrc.js', '*.spec.ts'], 33 | }; -------------------------------------------------------------------------------- /packages/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@multiversx/sdk-nestjs-common", 3 | "version": "5.0.0", 4 | "description": "Multiversx SDK Nestjs common package", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/**/*" 9 | ], 10 | "scripts": { 11 | "test": "jest --config=../../jest.config.json --passWithNoTests common/*", 12 | "build": "tsc", 13 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", 14 | "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/multiversx/mx-sdk-nestjs.git" 19 | }, 20 | "keywords": [ 21 | "multiversx", 22 | "nestjs", 23 | "common" 24 | ], 25 | "author": "MultiversX", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@types/node": "^16.11.10", 29 | "@types/uuid": "^8.3.4", 30 | "@typescript-eslint/eslint-plugin": "^5.12.0", 31 | "@typescript-eslint/parser": "^5.16.0", 32 | "eslint": "^8.9.0", 33 | "typescript": "^4.3.5" 34 | }, 35 | "dependencies": { 36 | "@multiversx/sdk-core": "^14.0.0", 37 | "nest-winston": "^1.6.2", 38 | "uuid": "^8.3.2", 39 | "winston": "^3.7.2" 40 | }, 41 | "peerDependencies": { 42 | "@multiversx/sdk-nestjs-monitoring": "^5.0.0", 43 | "@nestjs/common": "^10.x", 44 | "@nestjs/config": "^3.x", 45 | "@nestjs/core": "^10.x", 46 | "@nestjs/swagger": "^7.x" 47 | }, 48 | "publishConfig": { 49 | "access": "public" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/common/src/common/complexity/apply.complexity.ts: -------------------------------------------------------------------------------- 1 | import { DecoratorUtils } from "../../utils/decorator.utils"; 2 | 3 | export class ApplyComplexityOptions { 4 | constructor(init?: Partial) { 5 | Object.assign(this, init); 6 | } 7 | 8 | target: any; 9 | } 10 | 11 | export const ApplyComplexity = DecoratorUtils.registerMethodDecorator(ApplyComplexityOptions); 12 | -------------------------------------------------------------------------------- /packages/common/src/common/complexity/complexity.estimation.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { DecoratorUtils } from "../../utils/decorator.utils"; 3 | 4 | export class ComplexityEstimationOptions { 5 | group?: string; 6 | value?: number; 7 | alternatives?: string[]; 8 | } 9 | 10 | export function ComplexityEstimation(options?: ComplexityEstimationOptions) { 11 | return DecoratorUtils.registerPropertyDecorator(ComplexityEstimationOptions, options); 12 | } 13 | -------------------------------------------------------------------------------- /packages/common/src/common/complexity/complexity.node.ts: -------------------------------------------------------------------------------- 1 | export class ComplexityNode { 2 | identifier: string; 3 | complexity: number; 4 | group: string | undefined; 5 | 6 | children: { [identifier: string]: ComplexityNode; }; 7 | 8 | constructor(identifier: string, complexity: number, group: string) { 9 | this.identifier = identifier; 10 | this.complexity = complexity; 11 | this.group = group; 12 | 13 | this.children = {}; 14 | } 15 | 16 | public addChild(identifier: string, complexity: number, group: string) { 17 | this.children[identifier] = new ComplexityNode(identifier, complexity, group); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/common/src/common/complexity/complexity.utils.ts: -------------------------------------------------------------------------------- 1 | import { DecoratorUtils } from "../../utils/decorator.utils"; 2 | import { ComplexityEstimationOptions } from "./complexity.estimation"; 3 | import { ComplexityTree } from "./complexity.tree"; 4 | 5 | export class ComplexityUtils { 6 | static updateComplexityTree(previousComplexity: any, target: any, fields: string[], size: number): ComplexityTree { 7 | const configuration = ComplexityUtils.getComplexityConfiguration(target); 8 | const complexityTree: ComplexityTree = previousComplexity?.tree ?? new ComplexityTree(); 9 | 10 | complexityTree.addChildNode(target.name, (configuration.default ?? 1) * size, "root"); 11 | 12 | for (const [field, estimation] of Object.entries(configuration.estimations ?? {})) { 13 | if (fields.find((item: any) => item === field)) { 14 | // @ts-ignore 15 | complexityTree.addChildNode(field, estimation.value, target.name, estimation.group); 16 | } 17 | } 18 | 19 | return complexityTree; 20 | } 21 | 22 | static getComplexityConfiguration(target: any): { [field: string]: number | any } { 23 | const configuration: { [key: string]: any } = { 24 | estimations: {}, 25 | }; 26 | 27 | const propertyConfiguration = DecoratorUtils.getPropertyDecorators(ComplexityEstimationOptions, target); 28 | if (propertyConfiguration) { 29 | for (const [field, estimation] of Object.entries(propertyConfiguration)) { 30 | configuration.estimations[field] = estimation; 31 | 32 | for (const alternative of estimation.alternatives ?? []) { 33 | configuration.estimations[alternative] = estimation; 34 | } 35 | } 36 | } 37 | 38 | return configuration; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/common/src/common/complexity/exceptions/complexity.exceeded.exception.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException } from "@nestjs/common"; 2 | 3 | export class ComplexityExceededException extends BadRequestException { 4 | constructor(complexity: number, threshold: number) { 5 | super(`Complexity ${complexity} exceeded threshold ${threshold}.`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/common/src/common/complexity/exceptions/parent.node.not.found.exception.ts: -------------------------------------------------------------------------------- 1 | export class ParentNodeNotFoundException extends Error { 2 | constructor(identifier: string) { 3 | super(`Parent node with identifier ${identifier} not found.`); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/common/src/common/config/base.config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@nestjs/common"; 2 | import { ConfigService } from "@nestjs/config"; 3 | import { BaseConfigUtils } from "./base.config.utils"; 4 | 5 | @Injectable() 6 | export class BaseConfigService { 7 | constructor(protected readonly configService: ConfigService) { } 8 | 9 | get(key: string): T | undefined { 10 | const keyOverride = BaseConfigUtils.getKeyOverride(key, (key) => this.configService.get(key)); 11 | if (keyOverride !== undefined) { 12 | return keyOverride as T; 13 | } 14 | 15 | const configValue = this.configService.get(key); 16 | 17 | const valueOverride = BaseConfigUtils.getValueOverride(key, configValue, (key) => this.configService.get(key)); 18 | if (valueOverride !== undefined) { 19 | return valueOverride as T; 20 | } 21 | 22 | return configValue; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/common/src/common/config/configuration.loader.error.ts: -------------------------------------------------------------------------------- 1 | export class ConfigurationLoaderError extends Error { 2 | constructor(readonly errors: any[]) { 3 | const message = ConfigurationLoaderError.getConsolidatedErrorMessage(errors); 4 | 5 | super(message); 6 | } 7 | 8 | private static getConsolidatedErrorMessage(errors: any[]) { 9 | return errors.map((error) => ConfigurationLoaderError.getErrorMessage(error)).join('\n'); 10 | } 11 | 12 | private static getErrorMessage(error: any) { 13 | const paramsString = Object.entries(error.params).map(([key, value]) => `${key}:${value}`).join(','); 14 | 15 | return `${error.keyword} error on path ${error.instancePath}: ${error.message} (${paramsString})`; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/common/src/common/config/configuration.loader.schema.type.ts: -------------------------------------------------------------------------------- 1 | export enum ConfigurationLoaderSchemaType { 2 | json = 'json', 3 | yaml = 'yaml', 4 | } 5 | -------------------------------------------------------------------------------- /packages/common/src/common/config/configuration.loader.settings.ts: -------------------------------------------------------------------------------- 1 | import { ConfigurationLoaderSchemaType } from "./configuration.loader.schema.type"; 2 | 3 | export class ConfigurationLoaderSettings { 4 | constructor(init?: Partial) { 5 | Object.assign(this, init); 6 | } 7 | 8 | configPath: string = ''; 9 | applyEnvOverrides: boolean = true; 10 | 11 | schemaPath?: string; 12 | schemaType: ConfigurationLoaderSchemaType = ConfigurationLoaderSchemaType.yaml; 13 | schemaExpand: boolean = true; 14 | } 15 | -------------------------------------------------------------------------------- /packages/common/src/common/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.config.service'; 2 | export * from './base.config.utils'; 3 | export * from './mxnest.config.service'; 4 | export * from './configuration.loader.error'; 5 | export * from './configuration.loader.schema.expander'; 6 | export * from './configuration.loader.schema.type'; 7 | export * from './configuration.loader.settings'; 8 | export * from './configuration.loader'; 9 | -------------------------------------------------------------------------------- /packages/common/src/common/config/mxnest.config.service.ts: -------------------------------------------------------------------------------- 1 | export interface MxnestConfigService { 2 | getSecurityAdmins(): string[]; 3 | 4 | getJwtSecret(): string; 5 | 6 | getApiUrl(): string; 7 | 8 | getNativeAuthMaxExpirySeconds(): number; 9 | 10 | getNativeAuthAcceptedOrigins(): string[]; 11 | } 12 | -------------------------------------------------------------------------------- /packages/common/src/common/entities/amount.ts: -------------------------------------------------------------------------------- 1 | export class Amount extends String { } 2 | -------------------------------------------------------------------------------- /packages/common/src/common/logging/logging.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { WinstonModule } from 'nest-winston'; 3 | import * as winston from 'winston'; 4 | 5 | @Module({ 6 | imports: [ 7 | WinstonModule.forRoot({ 8 | level: 'verbose', 9 | format: winston.format.combine(winston.format.timestamp(), winston.format.json()), 10 | transports: [ 11 | new winston.transports.Console({ level: 'info' }), 12 | ], 13 | }), 14 | ], 15 | exports: [ 16 | WinstonModule, 17 | ], 18 | }) 19 | export class LoggingModule { } 20 | -------------------------------------------------------------------------------- /packages/common/src/common/shutdown-aware/index.ts: -------------------------------------------------------------------------------- 1 | export * from './shutdown-aware.handler'; 2 | export * from './shutdown-aware'; 3 | export * from './shutting-down.error'; 4 | -------------------------------------------------------------------------------- /packages/common/src/common/shutdown-aware/shutdown-aware.ts: -------------------------------------------------------------------------------- 1 | import { ShutdownAwareHandler } from "./shutdown-aware.handler"; 2 | 3 | export function ShutdownAware() { 4 | return (_target: Object, _key: string | symbol, descriptor: PropertyDescriptor) => { 5 | const childMethod = descriptor.value; 6 | 7 | descriptor.value = async function (...args: any[]) { 8 | await ShutdownAwareHandler.executeCriticalTask(async () => await childMethod.apply(this, args)); 9 | }; 10 | 11 | return descriptor; 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/common/src/common/shutdown-aware/shutting-down.error.ts: -------------------------------------------------------------------------------- 1 | export class ShuttingDownError extends Error { 2 | constructor() { 3 | super('Shutting down'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/common/src/common/swappable-settings/entities/constants.ts: -------------------------------------------------------------------------------- 1 | export const SWAPPABLE_SETTINGS_STORAGE_CLIENT = 'SWAPPABLE_SETTINGS_STORAGE_CLIENT'; 2 | -------------------------------------------------------------------------------- /packages/common/src/common/swappable-settings/entities/swappable-settings-async-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata } from "@nestjs/common"; 2 | import { SwappableSettingsStorageInterface } from "./swappable-settings-storage.interface"; 3 | 4 | export interface SwappableSettingsAsyncOptions extends Pick { 5 | inject?: any[]; 6 | useFactory?: (...args: any[]) => Promise | SwappableSettingsStorageInterface; 7 | } 8 | -------------------------------------------------------------------------------- /packages/common/src/common/swappable-settings/entities/swappable-settings-storage.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SwappableSettingsStorageInterface { 2 | set: (key: string, value: string, redisEx?: string, redisTtl?: number) => Promise, 3 | get: (key: string) => Promise, 4 | delete: (key: string) => Promise 5 | } 6 | -------------------------------------------------------------------------------- /packages/common/src/common/swappable-settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './swappable-settings.controller'; 2 | export * from './swappable-settings.service'; 3 | export * from './swappable-settings.module'; 4 | export * from './entities/constants'; 5 | export * from './entities/swappable-settings-storage.interface'; 6 | export * from './entities/swappable-settings-async-options.interface'; 7 | -------------------------------------------------------------------------------- /packages/common/src/common/swappable-settings/swappable-settings.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post, Delete, Param, Get } from '@nestjs/common'; 2 | import { SwappableSettingsService } from './swappable-settings.service'; 3 | 4 | @Controller() 5 | export class SwappableSettingsController { 6 | constructor( 7 | private readonly swappableSettingsService: SwappableSettingsService, 8 | ) { } 9 | 10 | @Post('/swappable-settings') 11 | setSetting( 12 | @Body() body: { key: string, value: string }, 13 | ): Promise { 14 | return this.swappableSettingsService.set(body.key, body.value); 15 | } 16 | 17 | @Get('/swappable-settings/:key') 18 | getSetting( 19 | @Param('key') key: string, 20 | ): Promise { 21 | return this.swappableSettingsService.get(key); 22 | } 23 | 24 | @Delete('/swappable-settings/:key') 25 | deleteSetting( 26 | @Param('key') key: string, 27 | ): Promise { 28 | return this.swappableSettingsService.delete(key); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/common/src/common/swappable-settings/swappable-settings.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Module } from '@nestjs/common'; 2 | import { SwappableSettingsService } from './swappable-settings.service'; 3 | import { SwappableSettingsStorageInterface } from './entities/swappable-settings-storage.interface'; 4 | import { SWAPPABLE_SETTINGS_STORAGE_CLIENT } from './entities/constants'; 5 | import { SwappableSettingsAsyncOptions } from './entities/swappable-settings-async-options.interface'; 6 | 7 | @Module({}) 8 | export class SwappableSettingsModule { 9 | public static forRoot(storage: SwappableSettingsStorageInterface): DynamicModule { 10 | return { 11 | module: SwappableSettingsModule, 12 | imports: [], 13 | providers: [ 14 | SwappableSettingsService, 15 | { 16 | provide: SWAPPABLE_SETTINGS_STORAGE_CLIENT, 17 | useValue: storage, 18 | }, 19 | ], 20 | exports: [ 21 | SwappableSettingsService, 22 | ], 23 | }; 24 | } 25 | 26 | public static forRootAsync(storageOptions: SwappableSettingsAsyncOptions): DynamicModule { 27 | return { 28 | module: SwappableSettingsModule, 29 | imports: storageOptions.imports || [], 30 | providers: [ 31 | { 32 | provide: SWAPPABLE_SETTINGS_STORAGE_CLIENT, 33 | useFactory: (factoryOptions) => factoryOptions, 34 | }, 35 | SwappableSettingsService, 36 | ], 37 | exports: [ 38 | SwappableSettingsService, 39 | ], 40 | }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/common/src/common/swappable-settings/swappable-settings.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { SWAPPABLE_SETTINGS_STORAGE_CLIENT } from './entities/constants'; 3 | import { SwappableSettingsStorageInterface } from './entities/swappable-settings-storage.interface'; 4 | 5 | @Injectable() 6 | export class SwappableSettingsService { 7 | private readonly prefix = 'swappable-setting:'; 8 | 9 | constructor( 10 | @Inject(SWAPPABLE_SETTINGS_STORAGE_CLIENT) private readonly storage: SwappableSettingsStorageInterface, 11 | ) { } 12 | 13 | public async get(key: string): Promise { 14 | const data = await this.storage.get(`${this.prefix}${key}`); 15 | return data; 16 | } 17 | 18 | public async set(key: string, value: string, redisEx?: string, redisTtl?: number): Promise { 19 | await this.storage.set(`${this.prefix}${key}`, value, redisEx, redisTtl); 20 | } 21 | 22 | public async delete(key: string): Promise { 23 | await this.storage.delete(`${this.prefix}${key}`); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/common/src/decorators/error.logger.ts: -------------------------------------------------------------------------------- 1 | import { OriginLogger } from "../utils/origin.logger"; 2 | 3 | interface IErrorLoggerOptions { 4 | logArgs: boolean; 5 | } 6 | 7 | const logger = new OriginLogger('Logger Decorator'); 8 | 9 | const getErrorText = (methodName: string, options?: IErrorLoggerOptions, ...args: any[]) => { 10 | const defaultText = `An unexpected error occurred when executing '${methodName}'`; 11 | 12 | if (options?.logArgs) 13 | return `${defaultText} with args ${args.join(',')}`; 14 | 15 | return defaultText; 16 | }; 17 | 18 | export function ErrorLoggerSync(options?: IErrorLoggerOptions) { 19 | return ( 20 | _target: Object, 21 | key: string | symbol, 22 | descriptor: PropertyDescriptor 23 | ) => { 24 | const childMethod = descriptor.value; 25 | descriptor.value = function (...args: any[]) { 26 | try { 27 | //@ts-ignore 28 | return childMethod.apply(this, args); 29 | } catch (error) { 30 | logger.error(getErrorText(String(key), options, ...args)); 31 | logger.error(error); 32 | throw error; 33 | } 34 | }; 35 | return descriptor; 36 | }; 37 | } 38 | 39 | export function ErrorLoggerAsync(options?: IErrorLoggerOptions) { 40 | return ( 41 | _target: Object, 42 | key: string | symbol, 43 | descriptor: PropertyDescriptor 44 | ) => { 45 | const childMethod = descriptor.value; 46 | descriptor.value = async function (...args: any[]) { 47 | try { 48 | //@ts-ignore 49 | return await childMethod.apply(this, args); 50 | } catch (error) { 51 | logger.error(getErrorText(String(key), options, ...args)); 52 | logger.error(error); 53 | throw error; 54 | } 55 | }; 56 | return descriptor; 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /packages/common/src/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lock'; 2 | export * from './error.logger'; 3 | export * from './passthrough'; 4 | export * from './swappable-setting'; 5 | -------------------------------------------------------------------------------- /packages/common/src/decorators/lock.ts: -------------------------------------------------------------------------------- 1 | import { Locker } from "../utils/locker"; 2 | 3 | export interface LockOptions { 4 | name?: string; 5 | verbose?: boolean; 6 | } 7 | 8 | export function Lock(options?: LockOptions) { 9 | return (_target: Object, _key: string | symbol, descriptor: PropertyDescriptor) => { 10 | const childMethod = descriptor.value; 11 | 12 | descriptor.value = async function (...args: any[]) { 13 | const lockerName = options?.name ?? childMethod.name; 14 | const verbose = options?.verbose ?? false; 15 | 16 | await Locker.lock( 17 | lockerName, 18 | async () => await childMethod.apply(this, args), 19 | verbose 20 | ); 21 | }; 22 | return descriptor; 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/common/src/decorators/passthrough.ts: -------------------------------------------------------------------------------- 1 | export function PassthroughAsync(enabled: boolean, returnedValue?: any) { 2 | return ( 3 | _target: Object, 4 | _key: string | symbol, 5 | descriptor: PropertyDescriptor 6 | ) => { 7 | const childMethod = descriptor.value; 8 | descriptor.value = async function (...args: any[]) { 9 | if (enabled) return returnedValue; 10 | return await childMethod.apply(this, args); 11 | }; 12 | return descriptor; 13 | }; 14 | } 15 | 16 | export function PassthroughSync(enabled: boolean, returnedValue: any) { 17 | return ( 18 | _target: Object, 19 | _key: string | symbol, 20 | descriptor: PropertyDescriptor 21 | ) => { 22 | const childMethod = descriptor.value; 23 | descriptor.value = function (...args: any[]) { 24 | if (enabled) return returnedValue; 25 | return childMethod.apply(this, args); 26 | }; 27 | return descriptor; 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /packages/common/src/decorators/swappable-setting.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { SwappableSettingsService } from '../common/swappable-settings'; 3 | 4 | export function SwappableSetting(settingKey: string) { 5 | const swappableSettingsServiceInjector = Inject(SwappableSettingsService); 6 | 7 | return function ( 8 | target: Object, 9 | _key: string | symbol, 10 | descriptor: PropertyDescriptor, 11 | ) { 12 | swappableSettingsServiceInjector(target, 'swappableSettingsService'); 13 | 14 | const childMethod = descriptor.value; 15 | descriptor.value = async function (...args: any[]) { 16 | //@ts-ignore 17 | const swappableSettingsService: SwappableSettingsService = this.swappableSettingsService; 18 | const setting = await swappableSettingsService.get(settingKey); 19 | if (setting) { 20 | return setting; 21 | } 22 | 23 | return await childMethod.apply(this, args); 24 | }; 25 | return descriptor; 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/common/src/pipes/entities/parse.array.options.ts: -------------------------------------------------------------------------------- 1 | export class ParseArrayPipeOptions { 2 | maxArraySize?: number; 3 | allowEmptyString: boolean; 4 | 5 | constructor(options: { allowEmptyString?: boolean; maxArraySize?: number } = {}) { 6 | this.allowEmptyString = options.allowEmptyString ?? false; 7 | this.maxArraySize = options.maxArraySize; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.address.and.metachain.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common"; 2 | import { AddressUtils } from "../utils/address.utils"; 3 | 4 | export class ParseAddressAndMetachainPipe implements PipeTransform> { 5 | transform(value: string | undefined, metadata: ArgumentMetadata): Promise { 6 | return new Promise(resolve => { 7 | if (value === undefined || value === '') { 8 | return resolve(undefined); 9 | } 10 | 11 | if (value == "4294967295") { 12 | return resolve(value); 13 | } 14 | 15 | if (AddressUtils.isAddressValid(value)) { 16 | return resolve(value); 17 | } 18 | 19 | throw new BadRequestException(`Validation failed for argument '${metadata.data}'. Address '${value}' is not valid`); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.address.array.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common"; 2 | import { AddressUtils } from "../utils/address.utils"; 3 | 4 | export class ParseAddressArrayPipe implements PipeTransform> { 5 | transform(value: string | undefined, metadata: ArgumentMetadata): Promise { 6 | return new Promise(resolve => { 7 | if (value === undefined || value === '') { 8 | return resolve(undefined); 9 | } 10 | 11 | const addresses = Array.isArray(value) ? value : value.split(','); 12 | 13 | for (const address of addresses) { 14 | if (!AddressUtils.isAddressValid(address)) { 15 | throw new BadRequestException(`Validation failed for argument '${metadata.data}'. Address '${address}' is not valid`); 16 | } 17 | } 18 | 19 | return resolve(addresses); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.address.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common"; 2 | import { AddressUtils } from "../utils/address.utils"; 3 | 4 | export class ParseAddressPipe implements PipeTransform> { 5 | transform(value: string | undefined, metadata: ArgumentMetadata): Promise { 6 | return new Promise(resolve => { 7 | if (value === undefined || value === '') { 8 | return resolve(undefined); 9 | } 10 | 11 | if (AddressUtils.isAddressValid(value)) { 12 | return resolve(value); 13 | } 14 | 15 | throw new BadRequestException(`Validation failed for argument '${metadata.data}' (a bech32 address is expected)`); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.array.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common"; 2 | import { ParseArrayPipeOptions } from "./entities/parse.array.options"; 3 | 4 | export class ParseArrayPipe implements PipeTransform> { 5 | private readonly options: ParseArrayPipeOptions; 6 | private readonly DEFAULT_MAX_ARRAY_SIZE = 1024; 7 | 8 | constructor(options?: Partial) { 9 | this.options = options ? new ParseArrayPipeOptions(options) : new ParseArrayPipeOptions(); 10 | } 11 | 12 | transform(value: string | undefined, metadata: ArgumentMetadata): Promise { 13 | return new Promise(resolve => { 14 | if (value === undefined || (!this.options.allowEmptyString && value === '')) { 15 | return resolve(undefined); 16 | } 17 | 18 | const valueArray = value.split(','); 19 | 20 | const maxArraySize = this.options.maxArraySize ?? this.DEFAULT_MAX_ARRAY_SIZE; 21 | 22 | if (valueArray.length > maxArraySize) { 23 | throw new BadRequestException(`Validation failed for argument '${metadata.data}' (less than ${maxArraySize} comma separated values expected)`); 24 | } 25 | 26 | const distinctValueArray = valueArray.distinct(); 27 | 28 | resolve(distinctValueArray); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.block.hash.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ParseHashPipe } from "./parse.hash.pipe"; 2 | 3 | export class ParseBlockHashPipe extends ParseHashPipe { 4 | constructor() { 5 | super('block', 64); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.bls.hash.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ParseHashPipe } from "./parse.hash.pipe"; 2 | 3 | export class ParseBlsHashPipe extends ParseHashPipe { 4 | constructor() { 5 | super('bls', 192); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.bool.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common"; 2 | 3 | export class ParseBoolPipe implements PipeTransform> { 4 | transform(value: string | boolean, metadata: ArgumentMetadata): Promise { 5 | return new Promise(resolve => { 6 | if (value === true || value === 'true') { 7 | return resolve(true); 8 | } 9 | 10 | if (value === false || value === 'false') { 11 | return resolve(false); 12 | } 13 | 14 | if (value === null || value === undefined || value === '') { 15 | return resolve(undefined); 16 | } 17 | 18 | throw new BadRequestException(`Validation failed for argument '${metadata.data}' (optional boolean string is expected)`); 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.collection.array.pipe.ts: -------------------------------------------------------------------------------- 1 | 2 | import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common"; 3 | import { TokenUtils } from "../utils/token.utils"; 4 | 5 | export class ParseCollectionArrayPipe implements PipeTransform> { 6 | 7 | transform(value: string | string[] | undefined, metadata: ArgumentMetadata): Promise { 8 | return new Promise((resolve) => { 9 | if (value === undefined || value === '') { 10 | return resolve(undefined); 11 | } 12 | 13 | const values = Array.isArray(value) ? value : value.split(','); 14 | 15 | for (const value of values) { 16 | if (!TokenUtils.isCollection(value)) { 17 | throw new BadRequestException(`Validation failed for '${metadata.data}'. Value ${value} does not represent a valid collection identifier`); 18 | } 19 | } 20 | 21 | return resolve(values); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.collection.pipe.ts: -------------------------------------------------------------------------------- 1 | import { TokenUtils } from "../utils/token.utils"; 2 | import { ParseRegexPipe } from "./parse.regex.pipe"; 3 | 4 | export class ParseCollectionPipe extends ParseRegexPipe { 5 | constructor() { 6 | super(TokenUtils.tokenValidateRegex, 'Invalid collection identifier'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.enum.array.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common"; 2 | 3 | export class ParseEnumArrayPipe implements PipeTransform> { 4 | constructor(private readonly type: T) { } 5 | 6 | transform(value: string | undefined, metadata: ArgumentMetadata): Promise { 7 | return new Promise(resolve => { 8 | if (value === undefined || value === '') { 9 | return resolve(undefined); 10 | } 11 | 12 | const values = Array.isArray(value) ? value : value.split(','); 13 | 14 | const expectedValues = this.getValues(this.type); 15 | for (const value of values) { 16 | if (!expectedValues.includes(value)) { 17 | throw new BadRequestException(`Validation failed for argument '${metadata.data}' (one of the following values is expected: ${expectedValues.join(', ')})`); 18 | } 19 | } 20 | 21 | return resolve(values); 22 | }); 23 | } 24 | 25 | 26 | private getValues(value: T): string[] { 27 | return Object.keys(value).map(key => value[key]).filter(value => typeof value === 'string') as string[]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.enum.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common"; 2 | 3 | export class ParseEnumPipe implements PipeTransform> { 4 | constructor(private readonly type: T) { } 5 | 6 | transform(value: string | undefined, metadata: ArgumentMetadata): Promise { 7 | return new Promise(resolve => { 8 | if (value === undefined || value === '') { 9 | return resolve(undefined); 10 | } 11 | 12 | const values = this.getValues(this.type); 13 | if (values.includes(value)) { 14 | return resolve(value); 15 | } 16 | 17 | throw new BadRequestException(`Validation failed for argument '${metadata.data}' (one of the following values is expected: ${values.join(', ')})`); 18 | }); 19 | } 20 | 21 | 22 | private getValues(value: T): string[] { 23 | return Object.keys(value).map(key => value[key]).filter(value => typeof value === 'string') as string[]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.hash.array.pipe.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentMetadata, 3 | BadRequestException, 4 | PipeTransform, 5 | } from "@nestjs/common"; 6 | 7 | export class ParseHashArrayPipe 8 | implements 9 | PipeTransform< 10 | string | string[] | undefined, 11 | Promise 12 | > 13 | { 14 | private entity: string; 15 | private length: number; 16 | 17 | constructor(entity: string, length: number) { 18 | this.entity = entity; 19 | this.length = length; 20 | } 21 | 22 | transform( 23 | value: string | string[] | undefined, 24 | metadata: ArgumentMetadata 25 | ): Promise { 26 | return new Promise((resolve) => { 27 | if (value === undefined || value === "") { 28 | return resolve(undefined); 29 | } 30 | 31 | const hashes = Array.isArray(value) ? value : value.split(","); 32 | 33 | for (const hash of hashes) { 34 | if (hash.length !== this.length) { 35 | throw new BadRequestException( 36 | `Validation failed for ${this.entity} hash '${metadata.data}'. Length should be ${this.length}.` 37 | ); 38 | } 39 | } 40 | 41 | return resolve(hashes); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.hash.pipe.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArgumentMetadata, 3 | BadRequestException, 4 | PipeTransform, 5 | } from "@nestjs/common"; 6 | 7 | export class ParseHashPipe 8 | implements 9 | PipeTransform< 10 | string | string[] | undefined, 11 | Promise 12 | > 13 | { 14 | private entity: string; 15 | private length: number; 16 | 17 | constructor(entity: string, length: number) { 18 | this.entity = entity; 19 | this.length = length; 20 | } 21 | 22 | transform( 23 | value: string | string[] | undefined, 24 | metadata: ArgumentMetadata 25 | ): Promise { 26 | return new Promise((resolve) => { 27 | if (value === undefined || value === "") { 28 | return resolve(undefined); 29 | } 30 | 31 | const values = Array.isArray(value) ? value : [value]; 32 | 33 | for (const _value of values) { 34 | if (_value.length !== this.length) { 35 | throw new BadRequestException( 36 | `Validation failed for ${this.entity} hash '${metadata.data}'. Length should be ${this.length}.` 37 | ); 38 | } 39 | } 40 | 41 | return resolve(value); 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.int.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common"; 2 | 3 | export class ParseIntPipe implements PipeTransform> { 4 | transform(value: string | undefined, metadata: ArgumentMetadata): Promise { 5 | return new Promise(resolve => { 6 | if (value === undefined || value === '') { 7 | return resolve(undefined); 8 | } 9 | 10 | if (!isNaN(Number(value))) { 11 | return resolve(Number(value)); 12 | } 13 | 14 | throw new BadRequestException(`Validation failed for argument '${metadata.data}' (optional number is expected)`); 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.nft.array.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common"; 2 | import { TokenUtils } from "../utils/token.utils"; 3 | 4 | export class ParseNftArrayPipe implements PipeTransform> { 5 | 6 | transform(value: string | string[] | undefined, metadata: ArgumentMetadata): Promise { 7 | return new Promise((resolve) => { 8 | if (value === undefined || value === '') { 9 | return resolve(undefined); 10 | } 11 | 12 | const values = Array.isArray(value) ? value : value.split(','); 13 | 14 | for (const value of values) { 15 | if (!TokenUtils.isNft(value)) { 16 | throw new BadRequestException(`Validation failed for '${metadata.data}'. Value ${value} does not represent a valid NFT identifier`); 17 | } 18 | } 19 | 20 | return resolve(values); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.nft.pipe.ts: -------------------------------------------------------------------------------- 1 | import { TokenUtils } from "../utils/token.utils"; 2 | import { ParseRegexPipe } from "./parse.regex.pipe"; 3 | 4 | export class ParseNftPipe extends ParseRegexPipe { 5 | constructor() { 6 | super(TokenUtils.nftValidateRegex, 'Invalid NFT identifier'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.record.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common"; 2 | 3 | export class ParseRecordPipe implements PipeTransform | undefined>> { 4 | transform(value: string | undefined, metadata: ArgumentMetadata): Promise | undefined> { 5 | return new Promise(resolve => { 6 | if (value === undefined || value === '') { 7 | return resolve(undefined); 8 | } 9 | 10 | const result: Record = {}; 11 | 12 | const entries = value.split(';'); 13 | 14 | for (const entry of entries) { 15 | const [key, value] = entry.split(':'); 16 | if (!key || !value) { 17 | throw new BadRequestException(`Validation failed for argument '${metadata.data}'. Value should be in the format ':;:'`); 18 | } 19 | 20 | result[key] = value; 21 | } 22 | 23 | return resolve(result); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.regex.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException, PipeTransform } from "@nestjs/common"; 2 | 3 | export class ParseRegexPipe implements PipeTransform> { 4 | private readonly regexes: RegExp[]; 5 | 6 | constructor( 7 | regex: RegExp | RegExp[], 8 | private readonly message: string = 'Invalid format' 9 | ) { 10 | if (Array.isArray(regex)) { 11 | this.regexes = regex; 12 | } else { 13 | this.regexes = [regex]; 14 | } 15 | } 16 | 17 | transform(value: string | undefined, metadata: ArgumentMetadata): Promise { 18 | return new Promise(resolve => { 19 | if (value === undefined || value === '') { 20 | return resolve(undefined); 21 | } 22 | 23 | for (const regex of this.regexes) { 24 | if (regex.test(value)) { 25 | return resolve(value); 26 | } 27 | } 28 | 29 | throw new BadRequestException(`Validation failed for argument '${metadata.data}': ${this.message}.`); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.token.or.nft.pipe.ts: -------------------------------------------------------------------------------- 1 | import { TokenUtils } from "../utils/token.utils"; 2 | import { ParseRegexPipe } from "./parse.regex.pipe"; 3 | 4 | export class ParseTokenOrNftPipe extends ParseRegexPipe { 5 | constructor() { 6 | super([TokenUtils.tokenValidateRegex, TokenUtils.nftValidateRegex], 'Invalid token / NFT identifier'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.token.pipe.ts: -------------------------------------------------------------------------------- 1 | import { TokenUtils } from "../utils/token.utils"; 2 | import { ParseRegexPipe } from "./parse.regex.pipe"; 3 | 4 | export class ParseTokenPipe extends ParseRegexPipe { 5 | constructor() { 6 | super(TokenUtils.tokenValidateRegex, 'Invalid token identifier'); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.transaction.hash.array.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ParseHashArrayPipe } from "./parse.hash.array.pipe"; 2 | 3 | export class ParseTranasctionHashArrayPipe extends ParseHashArrayPipe { 4 | constructor() { 5 | super('transaction', 64); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/common/src/pipes/parse.transaction.hash.pipe.ts: -------------------------------------------------------------------------------- 1 | import { ParseHashPipe } from "./parse.hash.pipe"; 2 | 3 | export class ParseTransactionHashPipe extends ParseHashPipe { 4 | constructor() { 5 | super('transaction', 64); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/common/src/sc.interactions/contract.loader.ts: -------------------------------------------------------------------------------- 1 | import { AbiRegistry, SmartContract, Address } from "@multiversx/sdk-core"; 2 | import * as fs from "fs"; 3 | import { OriginLogger } from "../utils/origin.logger"; 4 | 5 | export class ContractLoader { 6 | private readonly logger = new OriginLogger(ContractLoader.name); 7 | private readonly abiPath: string; 8 | private abi: AbiRegistry | undefined = undefined; 9 | 10 | constructor(abiPath: string, _contractInterface?: string) { 11 | this.abiPath = abiPath; 12 | } 13 | 14 | private async load(): Promise { 15 | try { 16 | const jsonContent: string = await fs.promises.readFile(this.abiPath, { encoding: "utf8" }); 17 | const json = JSON.parse(jsonContent); 18 | 19 | const abiRegistry = AbiRegistry.create(json); 20 | 21 | return abiRegistry; 22 | } catch (error) { 23 | this.logger.log(`Unexpected error when trying to create smart contract from abi`); 24 | this.logger.error(error); 25 | 26 | throw new Error('Error when creating contract from abi'); 27 | } 28 | } 29 | 30 | async getContract(contractAddress: string): Promise { 31 | if (!this.abi) { 32 | this.abi = await this.load(); 33 | } 34 | 35 | return new SmartContract({ 36 | address: new Address(contractAddress), 37 | abi: this.abi, 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/common/src/sc.interactions/contract.query.runner.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Abi, 3 | INetworkProvider, 4 | SmartContractController, 5 | SmartContractQuery, 6 | SmartContractQueryResponse, 7 | } from "@multiversx/sdk-core"; 8 | import { OriginLogger } from "../utils/origin.logger"; 9 | 10 | export class ContractQueryRunner { 11 | private readonly logger = new OriginLogger(ContractQueryRunner.name); 12 | private readonly proxy: INetworkProvider; 13 | 14 | constructor(proxy: INetworkProvider) { 15 | this.proxy = proxy; 16 | } 17 | 18 | async runQuery( 19 | query: SmartContractQuery, 20 | chainID: string, 21 | abi?: Abi 22 | ): Promise { 23 | try { 24 | const controller = new SmartContractController({ 25 | chainID: chainID, 26 | networkProvider: this.proxy, 27 | abi: abi, 28 | }); 29 | const response = await controller.runQuery(query); 30 | 31 | return response; 32 | } catch (error) { 33 | this.logger.log( 34 | `Unexpected error when running query '${ 35 | query.function 36 | }' to sc '${query.contract.toBech32()}' ` 37 | ); 38 | this.logger.error(error); 39 | 40 | throw error; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/common/src/sc.interactions/index.ts: -------------------------------------------------------------------------------- 1 | export * from './contract.loader'; 2 | export * from './contract.query.runner'; 3 | export * from './contract.transaction.generator'; 4 | -------------------------------------------------------------------------------- /packages/common/src/utils/binary.utils.ts: -------------------------------------------------------------------------------- 1 | import { AddressUtils } from "./address.utils"; 2 | 3 | function base64DecodeBinary(str: string): Buffer { 4 | return Buffer.from(str, "base64"); 5 | } 6 | 7 | export class BinaryUtils { 8 | static base64Encode(str: string) { 9 | return Buffer.from(str).toString("base64"); 10 | } 11 | 12 | static base64Decode(str: string): string { 13 | return base64DecodeBinary(str).toString("binary"); 14 | } 15 | 16 | static tryBase64ToBigInt(str: string): BigInt | undefined { 17 | try { 18 | return this.base64ToBigInt(str); 19 | } catch { 20 | return undefined; 21 | } 22 | } 23 | 24 | static base64ToBigInt(str: string): BigInt { 25 | const hex = this.base64ToHex(str); 26 | return BigInt(hex ? "0x" + hex : hex); 27 | } 28 | 29 | static tryBase64ToHex(str: string): string | undefined { 30 | try { 31 | return this.base64ToHex(str); 32 | } catch { 33 | return undefined; 34 | } 35 | } 36 | 37 | static base64ToHex(str: string): string { 38 | return Buffer.from(str, "base64").toString("hex"); 39 | } 40 | 41 | static stringToHex(str: string): string { 42 | return Buffer.from(str).toString("hex"); 43 | } 44 | 45 | static tryBase64ToAddress(str: string): string | undefined { 46 | try { 47 | return this.base64ToAddress(str); 48 | } catch { 49 | return undefined; 50 | } 51 | } 52 | 53 | static base64ToAddress(str: string): string { 54 | return AddressUtils.bech32Encode(this.base64ToHex(str)); 55 | } 56 | 57 | static hexToString(hex: string): string { 58 | return Buffer.from(hex, "hex").toString("ascii"); 59 | } 60 | 61 | static hexToNumber(hex: string): number { 62 | return parseInt(hex, 16); 63 | } 64 | 65 | static hexToBase64(hex: string): string { 66 | return Buffer.from(hex, "hex").toString("base64"); 67 | } 68 | 69 | static hexToBigInt(hex: string): BigInt { 70 | if (!hex) { 71 | return BigInt(0); 72 | } 73 | 74 | return BigInt("0x" + hex); 75 | } 76 | 77 | static padHex(value: string): string { 78 | return value.length % 2 ? "0" + value : value; 79 | } 80 | 81 | static numberToHex(value: number): string { 82 | return BinaryUtils.padHex(value.toString(16)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /packages/common/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export class Constants { 2 | static oneSecond(): number { 3 | return 1; 4 | } 5 | 6 | static oneMinute(): number { 7 | return Constants.oneSecond() * 60; 8 | } 9 | 10 | static oneHour(): number { 11 | return Constants.oneMinute() * 60; 12 | } 13 | 14 | static oneDay(): number { 15 | return Constants.oneHour() * 24; 16 | } 17 | 18 | static oneWeek(): number { 19 | return Constants.oneDay() * 7; 20 | } 21 | 22 | static oneMonth(): number { 23 | return Constants.oneDay() * 30; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/common/src/utils/context.tracker.ts: -------------------------------------------------------------------------------- 1 | import async_hooks from 'async_hooks'; 2 | import { randomUUID } from 'crypto'; 3 | 4 | export class ContextTracker { 5 | private static readonly asyncHookDict: Record = {}; 6 | private static readonly contextDict: Record = {}; 7 | private static hook?: async_hooks.AsyncHook; 8 | 9 | static assign(value: Object) { 10 | ContextTracker.ensureIsTracking(); 11 | 12 | const asyncId = async_hooks.executionAsyncId(); 13 | let contextId = ContextTracker.asyncHookDict[asyncId]; 14 | if (!contextId) { 15 | contextId = randomUUID(); 16 | 17 | ContextTracker.asyncHookDict[asyncId] = contextId; 18 | } 19 | 20 | let context = ContextTracker.contextDict[contextId]; 21 | if (!context) { 22 | context = {}; 23 | 24 | ContextTracker.contextDict[contextId] = context; 25 | } 26 | 27 | Object.assign(context, value); 28 | } 29 | 30 | static get() { 31 | const asyncId = async_hooks.executionAsyncId(); 32 | const contextId = ContextTracker.asyncHookDict[asyncId]; 33 | if (!contextId) { 34 | return undefined; 35 | } 36 | 37 | return ContextTracker.contextDict[contextId]; 38 | } 39 | 40 | static unassign() { 41 | const asyncId = async_hooks.executionAsyncId(); 42 | const contextId = ContextTracker.asyncHookDict[asyncId]; 43 | if (!contextId) { 44 | return; 45 | } 46 | 47 | delete this.contextDict[contextId]; 48 | } 49 | 50 | private static ensureIsTracking(): async_hooks.AsyncHook { 51 | if (!ContextTracker.hook) { 52 | ContextTracker.hook = async_hooks.createHook({ init: onInit, destroy: onDestroy }).enable(); 53 | } 54 | 55 | return ContextTracker.hook; 56 | 57 | // eslint-disable-next-line @typescript-eslint/no-this-alias 58 | function onInit(asyncId: number, _: string, triggerAsyncId: number) { 59 | const previousValue = ContextTracker.asyncHookDict[triggerAsyncId]; 60 | if (previousValue) { 61 | ContextTracker.asyncHookDict[asyncId] = previousValue; 62 | } 63 | } 64 | 65 | function onDestroy(asyncId: number) { 66 | delete ContextTracker.asyncHookDict[asyncId]; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/common/src/utils/date.utils.ts: -------------------------------------------------------------------------------- 1 | export class DateUtils { 2 | static createUTC(year: number, month?: number, day?: number, hours?: number, minutes?: number, seconds?: number, milliseconds?: number): Date { 3 | const date = new Date(); 4 | date.setUTCFullYear(year, (month ?? 1) - 1, day ?? 1); 5 | date.setUTCHours(hours ?? 0, minutes ?? 0, seconds ?? 0, milliseconds ?? 0); 6 | 7 | return date; 8 | } 9 | 10 | static create(year: number, month?: number, day?: number, hours?: number, minutes?: number, seconds?: number, milliseconds?: number): Date { 11 | const date = new Date(); 12 | date.setFullYear(year, (month ?? 1) - 1, day ?? 1); 13 | date.setHours(hours ?? 0, minutes ?? 0, seconds ?? 0, milliseconds ?? 0); 14 | 15 | return date; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/common/src/utils/decorator.utils.ts: -------------------------------------------------------------------------------- 1 | import { Type } from "@nestjs/common"; 2 | 3 | export class DecoratorUtils { 4 | static registerMethodDecorator(type: Type): (options?: T) => MethodDecorator { 5 | return (options?: T): MethodDecorator => (_, __, descriptor: any) => { 6 | // @ts-ignore 7 | Reflect.defineMetadata(type.name, Object.assign(new type(), options), descriptor.value); 8 | return descriptor; 9 | }; 10 | } 11 | 12 | static registerClassDecorator( 13 | type: Type, 14 | metadata: T 15 | ): ClassDecorator { 16 | return (target) => { 17 | Reflect.defineMetadata(type.name, metadata, target); 18 | return target; 19 | }; 20 | } 21 | 22 | static registerPropertyDecorator( 23 | type: Type, 24 | metadata: T 25 | ): PropertyDecorator { 26 | return (target, key) => { 27 | let existingMetadata = Reflect.getMetadata(type.name, target.constructor); 28 | if (!existingMetadata) { 29 | existingMetadata = {}; 30 | } 31 | 32 | existingMetadata[key] = metadata; 33 | 34 | Reflect.defineMetadata(type.name, existingMetadata, target.constructor); 35 | }; 36 | } 37 | 38 | static getMethodDecorator(type: Type, target: Function): T | undefined { 39 | const metadata = Reflect.getOwnMetadata(type.name, target); 40 | if (!metadata) { 41 | return undefined; 42 | } 43 | 44 | if (!(metadata instanceof type)) { 45 | return undefined; 46 | } 47 | 48 | return metadata; 49 | } 50 | 51 | static getClassDecorator(type: Type, target: Type): TResult | undefined { 52 | return Reflect.getOwnMetadata(type.name, target); 53 | } 54 | 55 | static getPropertyDecorators(type: Type, target: Type): Record { 56 | return Reflect.getMetadata(type.name, target); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/common/src/utils/execution.context.utils.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext } from "@nestjs/common"; 2 | 3 | export class ExecutionContextUtils { 4 | static getHeaders(context: ExecutionContext): Record { 5 | const contextType: string = context.getType(); 6 | if (["http", "https"].includes(contextType)) { 7 | const request = context.switchToHttp().getRequest(); 8 | 9 | return request.headers; 10 | } 11 | 12 | if (contextType === "graphql") { 13 | return context.getArgByIndex(2).req.headers; 14 | } 15 | 16 | return {}; 17 | } 18 | 19 | static getResponse(context: ExecutionContext): any { 20 | const contextType: string = context.getType(); 21 | if (["http", "https"].includes(contextType)) { 22 | const request = context.switchToHttp().getRequest(); 23 | 24 | return request.res; 25 | } 26 | 27 | return undefined; 28 | } 29 | 30 | static getRequest(context: ExecutionContext): any { 31 | const contextType: string = context.getType(); 32 | if (["http", "https"].includes(contextType)) { 33 | const request = context.switchToHttp().getRequest(); 34 | 35 | return request; 36 | } 37 | 38 | if (contextType === "graphql") { 39 | return context.getArgByIndex(2).req; 40 | } 41 | 42 | return undefined; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/common/src/utils/extensions/jest.extensions.ts: -------------------------------------------------------------------------------- 1 | expect.extend({ 2 | toHaveStructure(received: any, keys: string[]) { 3 | const objectSortedKeys = JSON.stringify(Object.keys(received).sort()); 4 | const expectedKeys = JSON.stringify(keys.sort()); 5 | 6 | const pass = objectSortedKeys === expectedKeys; 7 | if (pass) { 8 | return { 9 | pass: true, 10 | message: () => `expected ${objectSortedKeys} not to have structure ${expectedKeys} `, 11 | }; 12 | } 13 | else { 14 | return { 15 | pass: false, 16 | message: () => `expected ${objectSortedKeys} to have structure ${expectedKeys} `, 17 | }; 18 | } 19 | }, 20 | 21 | toHaveProperties(received: any, args: any[]) { 22 | const receivedProperties = Object.getOwnPropertyNames(received); 23 | const pass = !args.some(val => receivedProperties.indexOf(val) === -1); 24 | if (pass) { 25 | return { 26 | message: () => `expected ${received} not to have properties of ${args}`, 27 | pass: true, 28 | }; 29 | } else { 30 | return { 31 | message: () => `expected ${received} to have properties of ${args}`, 32 | pass: false, 33 | }; 34 | } 35 | }, 36 | }); 37 | 38 | interface Matchers { 39 | toHaveStructure(received: any, keys: string[]): R; 40 | toHaveProperties(received: any, args: any[]): R; 41 | } 42 | -------------------------------------------------------------------------------- /packages/common/src/utils/extensions/number.extensions.ts: -------------------------------------------------------------------------------- 1 | Number.prototype.toRounded = function (digits?: number): number { 2 | return parseFloat(this.toFixed(digits ?? 0)); 3 | }; 4 | 5 | Number.prototype.in = function (...elements: number[]): boolean { 6 | return elements.includes(this.valueOf()); 7 | }; 8 | 9 | declare interface Number { 10 | toRounded(digits?: number): number; 11 | in(...elements: number[]): boolean; 12 | } 13 | -------------------------------------------------------------------------------- /packages/common/src/utils/extensions/string.extensions.ts: -------------------------------------------------------------------------------- 1 | String.prototype.removePrefix = function (prefix: string): string { 2 | if (this.startsWith(prefix)) { 3 | return this.slice(prefix.length); 4 | } 5 | 6 | return this.toString(); 7 | }; 8 | 9 | String.prototype.removeSuffix = function (suffix: string): string { 10 | if (this.endsWith(suffix)) { 11 | return this.slice(0, -suffix.length); 12 | } 13 | 14 | return this.toString(); 15 | }; 16 | 17 | String.prototype.in = function (...elements: string[]): boolean { 18 | return elements.includes(this.valueOf()); 19 | }; 20 | 21 | declare interface String { 22 | removePrefix(prefix: string): string; 23 | removeSuffix(suffix: string): string; 24 | in(...elements: string[]): boolean; 25 | } 26 | -------------------------------------------------------------------------------- /packages/common/src/utils/file.utils.ts: -------------------------------------------------------------------------------- 1 | const { readdirSync, readFileSync } = require('fs'); 2 | const { readFile, stat, unlink, writeFile } = require('fs').promises; 3 | import { Logger } from '@nestjs/common'; 4 | 5 | export class FileUtils { 6 | static getDirectories(source: string) { 7 | return readdirSync(source, { withFileTypes: true }) 8 | .filter((dirent: any) => dirent.isDirectory()) 9 | .map((dirent: any) => dirent.name); 10 | } 11 | 12 | static getFiles(source: string) { 13 | return readdirSync(source, { withFileTypes: true }) 14 | .filter((dirent: any) => !dirent.isDirectory()) 15 | .map((dirent: any) => dirent.name); 16 | } 17 | 18 | static parseJSONFile(source: string) { 19 | return JSON.parse(readFileSync(source, { encoding: 'utf8' })); 20 | } 21 | 22 | static async writeFile(buffer: Buffer, path: string): Promise { 23 | await writeFile(path, buffer); 24 | } 25 | 26 | static async readFile(path: string): Promise { 27 | return await readFile(path); 28 | } 29 | 30 | static async deleteFile(path: string): Promise { 31 | await unlink(path); 32 | } 33 | 34 | static async getFileSize(path: string): Promise { 35 | const statistics = await stat(path); 36 | return statistics.size; 37 | } 38 | 39 | static async exists(path: string): Promise { 40 | try { 41 | await stat(path); 42 | return true; 43 | } catch (error: any) { 44 | if (error.code !== 'ENOENT') { 45 | const logger = new Logger(FileUtils.name); 46 | logger.error(`Unknown error when performing file exists check`); 47 | logger.error(error); 48 | } 49 | 50 | return false; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/common/src/utils/locker.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "@nestjs/common"; 2 | import { MetricsService, CpuProfiler, PerformanceProfiler } from "@multiversx/sdk-nestjs-monitoring"; 3 | import { ContextTracker } from "./context.tracker"; 4 | 5 | export class Locker { 6 | private static lockSet: Set = new Set(); 7 | 8 | static async lock(key: string, func: () => Promise, log: boolean = false): Promise { 9 | const logger = new Logger('Lock'); 10 | 11 | if (Locker.lockSet.has(key)) { 12 | logger.log(`${key} is already running`); 13 | return LockResult.ALREADY_RUNNING; 14 | } 15 | 16 | Locker.lockSet.add(key); 17 | 18 | const profiler = new PerformanceProfiler(); 19 | const cpuProfiler = log ? new CpuProfiler() : undefined; 20 | 21 | ContextTracker.assign({ origin: key }); 22 | 23 | try { 24 | await func(); 25 | 26 | profiler.stop(); 27 | cpuProfiler?.stop(log ? `Running ${key}` : undefined); 28 | 29 | MetricsService.setJobResult(key, 'success', profiler.duration); 30 | 31 | return LockResult.SUCCESS; 32 | } catch (error) { 33 | logger.error(`Error running ${key}`); 34 | logger.error(error); 35 | 36 | profiler.stop(); 37 | cpuProfiler?.stop(log ? `Running ${key}` : undefined); 38 | 39 | MetricsService.setJobResult(key, 'error', profiler.duration); 40 | 41 | return LockResult.ERROR; 42 | } finally { 43 | Locker.lockSet.delete(key); 44 | } 45 | } 46 | } 47 | 48 | export enum LockResult { 49 | SUCCESS = 'success', 50 | ALREADY_RUNNING = 'alreadyRunning', 51 | ERROR = 'error' 52 | } 53 | -------------------------------------------------------------------------------- /packages/common/src/utils/logger.initializer.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "@nestjs/common"; 2 | 3 | export class LoggerInitializer { 4 | static initialize(logger: Logger) { 5 | Logger.overrideLogger(logger); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/common/src/utils/match.utils.ts: -------------------------------------------------------------------------------- 1 | import { BinaryUtils } from "./binary.utils"; 2 | 3 | export class MatchUtils { 4 | static getTagsFromBase64Attributes(attributes: string) { 5 | const decodedAttributes = BinaryUtils.base64Decode(attributes); 6 | return decodedAttributes.match(/tags:(?[\w\s,\-]*)/); 7 | } 8 | 9 | static getMetadataFromBase64Attributes(attributes: string) { 10 | const decodedAttributes = BinaryUtils.base64Decode(attributes); 11 | return decodedAttributes.match(/metadata:(?[\w\/\.]*)/); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/common/src/utils/mxnest.constants.ts: -------------------------------------------------------------------------------- 1 | export const MXNEST_CONFIG_SERVICE = 'MxnestConfigService'; 2 | -------------------------------------------------------------------------------- /packages/common/src/utils/number.utils.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from 'bignumber.js'; 2 | export class NumberUtils { 3 | static denominate(value: BigInt, decimals: number = 18): number { 4 | return new BigNumber(value.toString()).dividedBy(new BigNumber(10).pow(decimals)).toNumber(); 5 | } 6 | 7 | static denominateString(value: string, decimals: number = 18): number { 8 | return NumberUtils.denominate(BigInt(value), decimals); 9 | } 10 | 11 | static toDenominatedString(amount: BigInt, decimals: number = 18): string { 12 | let denominatedValue = new BigNumber(amount.toString()).shiftedBy(-decimals).toFixed(decimals); 13 | if (denominatedValue.includes('.')) { 14 | denominatedValue = denominatedValue.replace(/0+$/g, '').replace(/\.$/g, ''); 15 | } 16 | 17 | return denominatedValue; 18 | } 19 | 20 | static numberDecode(encoded: string): string { 21 | const hex = Buffer.from(encoded, 'base64').toString('hex'); 22 | return new BigNumber(hex, 16).toString(10); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/common/src/utils/origin.logger.ts: -------------------------------------------------------------------------------- 1 | import { Logger, LoggerService } from "@nestjs/common"; 2 | import { ContextTracker } from "./context.tracker"; 3 | 4 | export class OriginLogger implements LoggerService { 5 | private readonly logger: Logger; 6 | 7 | constructor( 8 | private readonly context: string 9 | ) { 10 | this.logger = new Logger(); 11 | } 12 | 13 | private getContext(): string { 14 | let actualContext = this.context; 15 | 16 | const trackedContext = ContextTracker.get(); 17 | if (trackedContext) { 18 | if (trackedContext.origin) { 19 | actualContext += ':' + trackedContext.origin; 20 | } 21 | 22 | if (trackedContext.requestId) { 23 | actualContext += ':' + trackedContext.requestId; 24 | } 25 | } 26 | 27 | return actualContext; 28 | } 29 | 30 | log(message: any, ...optionalParams: any[]) { 31 | this.logger.log(message, ...optionalParams, this.getContext()); 32 | } 33 | 34 | error(message: any, ...optionalParams: any[]) { 35 | this.logger.error(message, ...optionalParams, message.stack ?? new Error().stack, this.getContext()); 36 | } 37 | 38 | warn(message: any, ...optionalParams: any[]) { 39 | this.logger.warn(message, ...optionalParams, this.getContext()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/common/src/utils/pending.executer.ts: -------------------------------------------------------------------------------- 1 | export class PendingExecuter { 2 | private readonly dictionary: Record> = {}; 3 | 4 | async execute( 5 | key: string, 6 | executer: () => Promise, 7 | ): Promise { 8 | const pendingRequest = this.dictionary[key]; 9 | if (pendingRequest) { 10 | return await pendingRequest; 11 | } 12 | 13 | try { 14 | return await (this.dictionary[key] = executer()); 15 | } finally { 16 | delete this.dictionary[key]; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/common/src/utils/record.utils.ts: -------------------------------------------------------------------------------- 1 | export class RecordUtils { 2 | static mapKeys(obj: Record, predicate: (key: string) => string): Record { 3 | const result: Record = {}; 4 | 5 | for (const key of Object.keys(obj)) { 6 | const newKey = predicate(key); 7 | 8 | result[newKey] = obj[key]; 9 | } 10 | 11 | return result; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/common/src/utils/round.utils.ts: -------------------------------------------------------------------------------- 1 | export class RoundUtils { 2 | static roundToEpoch(round: number): number { 3 | return Math.floor(round / 14401); 4 | } 5 | 6 | static getExpires(epochs: number, roundsPassed: number, roundsPerEpoch: number, roundDuration: number) { 7 | const now = Math.floor(Date.now() / 1000); 8 | 9 | if (epochs === 0) { 10 | return now; 11 | } 12 | 13 | const fullEpochs = (epochs - 1) * roundsPerEpoch * roundDuration; 14 | const lastEpoch = (roundsPerEpoch - roundsPassed) * roundDuration; 15 | 16 | return now + fullEpochs + lastEpoch; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/common/src/utils/string.utils.ts: -------------------------------------------------------------------------------- 1 | export class StringUtils { 2 | static isFunctionName(value: string) { 3 | return new RegExp(/[^a-z0-9_]/gi).test(value) === false; 4 | } 5 | 6 | static isHex(value: string) { 7 | return new RegExp(/[^a-f0-9]/gi).test(value) === false; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/common/src/utils/swagger.utils.ts: -------------------------------------------------------------------------------- 1 | import { ApiPropertyOptions } from "@nestjs/swagger"; 2 | import { Amount } from "../common/entities/amount"; 3 | 4 | export class SwaggerUtils { 5 | static amountPropertyOptions(extraOptions?: ApiPropertyOptions): ApiPropertyOptions { 6 | return { 7 | type: Amount, 8 | example: `\"${(Math.round(Math.random() * (10 ** 3))) * (10 ** 16)}\"`, 9 | title: 'Amount', 10 | ...extraOptions, 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/common/src/utils/token.utils.ts: -------------------------------------------------------------------------------- 1 | export class TokenUtils { 2 | static tokenValidateRegex: RegExp = /^[A-Za-z0-9]{3,10}-[a-fA-F0-9]{6}$/; 3 | static nftValidateRegex: RegExp = /^[A-Za-z0-9]{3,10}-[a-fA-F0-9]{6}-[a-fA-F0-9]{2,}$/; 4 | 5 | static isToken(identifier: string): boolean { 6 | return this.tokenValidateRegex.test(identifier); 7 | } 8 | 9 | static isCollection(identifier: string): boolean { 10 | return this.tokenValidateRegex.test(identifier); 11 | } 12 | 13 | static isNft(identifier: string): boolean { 14 | return this.nftValidateRegex.test(identifier); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/common/src/utils/url.utils.ts: -------------------------------------------------------------------------------- 1 | export class UrlUtils { 2 | static isLocalhost(url: string): boolean { 3 | try { 4 | const requestUrl = new URL(url); 5 | return requestUrl.hostname === 'localhost'; 6 | } catch { 7 | return false; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/common/test/extensions/date.extensions.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../src/utils/extensions/date.extensions'; 2 | 3 | describe('Date Extensions', () => { 4 | it('getTimeInSeconds', () => { 5 | expect(new Date(1712051285123).getTimeInSeconds()).toEqual(1712051285); 6 | expect(new Date(1712051285999).getTimeInSeconds()).toEqual(1712051285); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/common/test/extensions/number.extensions.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../src/utils/extensions/number.extensions'; 2 | 3 | describe('Number Extensions', () => { 4 | it('toRounded', () => { 5 | expect(3.1415.toRounded()).toEqual(3); 6 | expect(3.1415.toRounded(1)).toEqual(3.1); 7 | expect(3.1415.toRounded(2)).toEqual(3.14); 8 | }); 9 | 10 | it('in', () => { 11 | expect(Number(3).in(1, 2, 3, 4)).toEqual(true); 12 | expect(Number(3).in(1, 2, 4)).toEqual(false); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/common/test/extensions/string.extensions.spec.ts: -------------------------------------------------------------------------------- 1 | import '../../src/utils/extensions/string.extensions'; 2 | 3 | describe('String Extensions', () => { 4 | it('removePrefix', () => { 5 | expect('helloworld'.removePrefix('hello')).toEqual('world'); 6 | expect('helloworld'.removePrefix('hello2')).toEqual('helloworld'); 7 | }); 8 | 9 | it('removeSuffix', () => { 10 | expect('helloworld'.removeSuffix('world')).toEqual('hello'); 11 | expect('helloworld'.removeSuffix('world2')).toEqual('helloworld'); 12 | }); 13 | 14 | it('in', () => { 15 | expect('hello'.in('hello', 'world')).toEqual(true); 16 | expect('world'.in('hello', 'world')).toEqual(true); 17 | expect('world2'.in('hello', 'world')).toEqual(false); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/common/test/number.extensions.spec.ts: -------------------------------------------------------------------------------- 1 | import '../src/utils/extensions/number.extensions'; 2 | 3 | describe('Number Extensions', () => { 4 | it('toRounded', () => { 5 | expect(3.1415.toRounded()).toEqual(3); 6 | expect(3.1415.toRounded(1)).toEqual(3.1); 7 | expect(3.1415.toRounded(2)).toEqual(3.14); 8 | }); 9 | 10 | it('in', () => { 11 | expect(Number(3).in(1, 2, 3, 4)).toEqual(true); 12 | expect(Number(3).in(1, 2, 4)).toEqual(false); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/common/test/pipes/parse.address.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata } from "@nestjs/common"; 2 | import { ParseAddressPipe } from "../../src/pipes/parse.address.pipe"; 3 | 4 | describe('ParseAddressPipe', () => { 5 | let target: ParseAddressPipe; 6 | 7 | beforeEach(() => { 8 | target = new ParseAddressPipe; 9 | }); 10 | 11 | describe('transform', () => { 12 | describe('when validation passes', () => { 13 | it('shoudl return address', async () => { 14 | const address: string = 'erd1qga7ze0l03chfgru0a32wxqf2226nzrxnyhzer9lmudqhjgy7ycqjjyknz'; 15 | expect(await target.transform(address, {} as ArgumentMetadata)).toStrictEqual('erd1qga7ze0l03chfgru0a32wxqf2226nzrxnyhzer9lmudqhjgy7ycqjjyknz'); 16 | }); 17 | }); 18 | 19 | describe('when validation fails', () => { 20 | // eslint-disable-next-line require-await 21 | it('should throw an error', async () => { 22 | return expect(target.transform('invalidAddress', {} as ArgumentMetadata)).rejects.toThrowError(); 23 | }); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /packages/common/test/pipes/parse.bool.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata } from "@nestjs/common"; 2 | import { ParseBoolPipe } from "../../src/pipes/parse.bool.pipe"; 3 | 4 | describe('ParseOptionalBoolPipe', () => { 5 | let target: ParseBoolPipe; 6 | 7 | beforeEach(() => { 8 | target = new ParseBoolPipe; 9 | }); 10 | 11 | describe('transform', () => { 12 | describe('when validation passes', () => { 13 | it('should return boolean', async () => { 14 | expect(await target.transform('true', {} as ArgumentMetadata)).toBeTruthy(); 15 | expect(await target.transform(true, {} as ArgumentMetadata)).toBeTruthy(); 16 | expect(await target.transform('false', {} as ArgumentMetadata)).toBeFalsy(); 17 | expect(await target.transform(false, {} as ArgumentMetadata)).toBeFalsy(); 18 | }); 19 | }); 20 | 21 | describe('when validation fails', () => { 22 | it('shoul throw an error', () => { 23 | return expect(target.transform('abc123', {} as ArgumentMetadata)).rejects.toThrowError(); 24 | }); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/common/test/pipes/parse.collection.array.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException } from "@nestjs/common"; 2 | import { ParseCollectionArrayPipe } from "../../../common/src/pipes/parse.collection.array.pipe"; 3 | 4 | describe('ParseCollectionArrayPipe', () => { 5 | let target: ParseCollectionArrayPipe; 6 | 7 | beforeEach(() => { 8 | target = new ParseCollectionArrayPipe(); 9 | }); 10 | 11 | describe('transform', () => { 12 | describe('when validation passes', () => { 13 | it('should return array of collection identifiers', async () => { 14 | const validCollectionIdentifier = 'ABCDE-efb116'; 15 | expect(await target.transform(validCollectionIdentifier, {} as ArgumentMetadata)).toStrictEqual([validCollectionIdentifier]); 16 | }); 17 | 18 | it('should return undefined for an empty string', async () => { 19 | expect(await target.transform('', {} as ArgumentMetadata)).toBeUndefined(); 20 | }); 21 | 22 | it('should return undefined for undefined value', async () => { 23 | expect(await target.transform(undefined, {} as ArgumentMetadata)).toBeUndefined(); 24 | }); 25 | }); 26 | 27 | describe('when validation fails', () => { 28 | it('should throw BadRequestException', async () => { 29 | const invalidCollectionIdentifier = 'ABCDE-efb116-02'; 30 | await expect(target.transform(invalidCollectionIdentifier, {} as ArgumentMetadata)).rejects.toThrow(BadRequestException); 31 | }); 32 | 33 | it('should throw BadRequestException even if array contains a valid collection identifier', async () => { 34 | const invalidCollectionIdentifiers = ['ABCDE-efb116-02', 'ABCDE-efb116']; 35 | await expect(target.transform(invalidCollectionIdentifiers, {} as ArgumentMetadata)).rejects.toThrow(BadRequestException); 36 | }); 37 | }); 38 | }); 39 | }); -------------------------------------------------------------------------------- /packages/common/test/pipes/parse.nft.array.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentMetadata, BadRequestException } from "@nestjs/common"; 2 | import { ParseNftArrayPipe } from "../../../common/src/pipes/parse.nft.array.pipe"; 3 | 4 | describe('ParseNftArrayPipe', () => { 5 | let target: ParseNftArrayPipe; 6 | 7 | beforeEach(() => { 8 | target = new ParseNftArrayPipe(); 9 | }); 10 | 11 | describe('transform', () => { 12 | describe('when validation passes', () => { 13 | it('should return array of NFT identifiers', async () => { 14 | const validNftIdentifier = 'ABCDE-efb116-02'; 15 | expect(await target.transform(validNftIdentifier, {} as ArgumentMetadata)).toStrictEqual([validNftIdentifier]); 16 | }); 17 | 18 | it('should throw BadRequestException even if array contains a valid identifier', async () => { 19 | const validNftIdentifier = 'ABCDE-efb116-02'; 20 | expect(await target.transform(validNftIdentifier, {} as ArgumentMetadata)).toStrictEqual([validNftIdentifier]); 21 | }); 22 | 23 | it('should return undefined for an empty string', async () => { 24 | expect(await target.transform('', {} as ArgumentMetadata)).toBeUndefined(); 25 | }); 26 | 27 | it('should return undefined for undefined value', async () => { 28 | expect(await target.transform(undefined, {} as ArgumentMetadata)).toBeUndefined(); 29 | }); 30 | }); 31 | 32 | describe('when validation fails', () => { 33 | it('should throw BadRequestException', async () => { 34 | const invalidNftIdentifier = 'ABCDE-efb116'; 35 | await expect(target.transform(invalidNftIdentifier, {} as ArgumentMetadata)).rejects.toThrow(BadRequestException); 36 | }); 37 | 38 | it('should throw BadRequestException even if array contains a valid identifier', async () => { 39 | const invalidNftIdentifiers = ['ABCDE-efb116-02', 'ABCDE-efb116']; 40 | await expect(target.transform(invalidNftIdentifiers, {} as ArgumentMetadata)).rejects.toThrow(BadRequestException); 41 | }); 42 | }); 43 | }); 44 | }); -------------------------------------------------------------------------------- /packages/common/test/sc.interactions/contract.transaction.generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountOnNetwork, 3 | Address, 4 | ApiNetworkProvider, 5 | NetworkConfig, 6 | } from "@multiversx/sdk-core"; 7 | import { ContractTransactionGenerator } from "../../src/sc.interactions/contract.transaction.generator"; 8 | 9 | const TEST_ADDRESS = 10 | "erd1wtm3yl58vcnj089lqy3tatkdpwklffh4pjnf27zwsa2znjyk355sutafqh"; 11 | describe("Contract transaction generator", () => { 12 | it("Should set transaction nonce", async () => { 13 | const cTxGenerator = new ContractTransactionGenerator( 14 | new ApiNetworkProvider("some-url") 15 | ); 16 | 17 | const getAccountSpy = jest 18 | .spyOn(ApiNetworkProvider.prototype, "getAccount") 19 | // eslint-disable-next-line require-await 20 | .mockImplementation( 21 | jest.fn( 22 | async (_: Address) => new AccountOnNetwork({ nonce: BigInt(10) }) 23 | ) 24 | ); 25 | 26 | const getNetworkConfigSpy = jest 27 | .spyOn(ApiNetworkProvider.prototype, "getNetworkConfig") 28 | // eslint-disable-next-line require-await 29 | .mockImplementation(jest.fn(async () => new NetworkConfig())); 30 | 31 | const tx = await cTxGenerator.createTransaction( 32 | { 33 | contract: new Address(TEST_ADDRESS), 34 | gasLimit: BigInt(20000000), 35 | function: "dummy", 36 | }, 37 | new Address(TEST_ADDRESS) 38 | ); 39 | 40 | expect(getAccountSpy).toHaveBeenCalled(); 41 | expect(getNetworkConfigSpy).toHaveBeenCalled(); 42 | 43 | expect(tx.nonce).toEqual(BigInt(10)); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/common/test/sc.interactions/test.abi.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildInfo": { 3 | "rustc": { 4 | "version": "1.61.0-nightly", 5 | "commitHash": "4ce3749235fc31d15ebd444b038a9877e8c700d7", 6 | "commitDate": "2022-02-28", 7 | "channel": "Nightly", 8 | "short": "rustc 1.61.0-nightly (4ce374923 2022-02-28)" 9 | }, 10 | "contractCrate": { 11 | "name": "metabonding", 12 | "version": "0.0.0" 13 | }, 14 | "framework": { 15 | "name": "elrond-wasm", 16 | "version": "0.30.0" 17 | } 18 | }, 19 | "docs": [ 20 | "Source code for the pause module:", 21 | "https://github.com/ElrondNetwork/elrond-wasm-rs/blob/master/elrond-wasm-modules/src/pause.rs" 22 | ], 23 | "name": "Metabonding", 24 | "constructor": { 25 | "inputs": [ 26 | { 27 | "name": "signer", 28 | "type": "Address" 29 | }, 30 | { 31 | "name": "opt_rewards_nr_first_grace_weeks", 32 | "type": "optional", 33 | "multi_arg": true 34 | }, 35 | { 36 | "name": "opt_first_week_start_epoch", 37 | "type": "optional", 38 | "multi_arg": true 39 | } 40 | ], 41 | "outputs": [] 42 | }, 43 | "endpoints": [ 44 | { 45 | "name": "getRewardsForWeek", 46 | "mutability": "readonly", 47 | "inputs": [ 48 | { 49 | "name": "week", 50 | "type": "u32" 51 | }, 52 | { 53 | "name": "user_delegation_amount", 54 | "type": "BigUint" 55 | }, 56 | { 57 | "name": "user_lkmex_staked_amount", 58 | "type": "BigUint" 59 | } 60 | ], 61 | "outputs": [ 62 | { 63 | "type": "variadic>", 64 | "multi_result": true 65 | } 66 | ] 67 | } 68 | ], 69 | "hasCallback": false, 70 | "types": {} 71 | } -------------------------------------------------------------------------------- /packages/common/test/string.extensions.spec.ts: -------------------------------------------------------------------------------- 1 | import '../src/utils/extensions/string.extensions'; 2 | 3 | describe('Number Extensions', () => { 4 | it('removePrefix', () => { 5 | expect('helloworld'.removePrefix('hello')).toEqual('world'); 6 | expect('helloworld'.removePrefix('hello2')).toEqual('helloworld'); 7 | }); 8 | 9 | it('removeSuffix', () => { 10 | expect('helloworld'.removeSuffix('world')).toEqual('hello'); 11 | expect('helloworld'.removeSuffix('world2')).toEqual('helloworld'); 12 | }); 13 | 14 | it('in', () => { 15 | expect('hello'.in('hello', 'world')).toEqual(true); 16 | expect('world'.in('hello', 'world')).toEqual(true); 17 | expect('world2'.in('hello', 'world')).toEqual(false); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/common/test/utils/const.utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from "../../src/utils/constants"; 2 | 3 | describe('Constants Utils', () => { 4 | describe('Constants conversion', () => { 5 | it('OneMinute', () => { 6 | expect(Constants.oneMinute()).toStrictEqual(Constants.oneSecond() * 60); 7 | }); 8 | it('OneHour', () => { 9 | expect(Constants.oneHour()).toStrictEqual(Constants.oneMinute() * 60); 10 | }); 11 | it('OneDay', () => { 12 | expect(Constants.oneDay()).toStrictEqual(Constants.oneHour() * 24); 13 | }); 14 | it('OneWeek', () => { 15 | expect(Constants.oneWeek()).toStrictEqual(Constants.oneDay() * 7); 16 | }); 17 | it('OneMonth', () => { 18 | expect(Constants.oneMonth()).toStrictEqual(Constants.oneDay() * 30); 19 | }); 20 | 21 | }); 22 | }); 23 | 24 | -------------------------------------------------------------------------------- /packages/common/test/utils/date.utils.spec.ts: -------------------------------------------------------------------------------- 1 | import {DateUtils} from '../../src/utils/date.utils'; 2 | 3 | describe('Date Utils', () => { 4 | it('createUTC', () => { 5 | const date = DateUtils.createUTC(2022, 10, 10, 18, 60, 60, 60); 6 | expect(new Date(date)).toBeInstanceOf(Date); 7 | }); 8 | 9 | it('create', () => { 10 | const date = DateUtils.create(2022, 10, 10, 18, 60, 60, 60); 11 | expect(new Date(date)).toBeInstanceOf(Date); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/common/test/utils/match.utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { MatchUtils } from "../../src/utils/match.utils"; 2 | 3 | describe('Match Utils', () => { 4 | it('getTagsFromBase64Attributes', () => { 5 | let match = MatchUtils.getTagsFromBase64Attributes('bWV0YWRhdGE6'); 6 | expect(match).toBeNull(); 7 | 8 | match = MatchUtils.getTagsFromBase64Attributes('bWV0YWRhdGE6YXNkYWRzYTt0YWdzOjEsMiwzLDQ='); 9 | expect(match).toBeDefined(); 10 | if (match?.groups) { 11 | expect(match.groups['tags']).toEqual('1,2,3,4'); 12 | } 13 | match = MatchUtils.getTagsFromBase64Attributes('dGFnczpuZnQtdGlja2V0LHJvYWQ7bWV0YWRhdGE6UW1SY1A5NGtYcjV6WmpSR3ZpN21KNnVuN0xweFVoWVZSNFI0UnBpY3h6Z1lrdA=='); 14 | expect(match).toBeDefined(); 15 | if (match?.groups) { 16 | expect(match.groups['tags']).toEqual('nft-ticket,road'); 17 | } 18 | }); 19 | it('getMetadataFromBase64Attributes', () => { 20 | let match = MatchUtils.getMetadataFromBase64Attributes('bWV0YWRhdGE6dGVzdDt0YWdzOjEsMiwzLDQ='); 21 | expect(match).toBeDefined(); 22 | if (match?.groups) { 23 | expect(match.groups['metadata']).toEqual('test'); 24 | } 25 | 26 | match = MatchUtils.getMetadataFromBase64Attributes('bWV0YWRhdGE6dGVzdC93aXRoLjt0YWdzOjEsMiwzLDQ='); 27 | expect(match).toBeDefined(); 28 | if (match?.groups) { 29 | expect(match.groups['metadata']).toEqual('test/with.'); 30 | } 31 | 32 | match = MatchUtils.getMetadataFromBase64Attributes('dGFnczoxLDIsMyw0'); 33 | expect(match).toBeNull(); 34 | }); 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /packages/common/test/utils/round.utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { RoundUtils } from "../../src/utils/round.utils"; 2 | 3 | describe('Round Utils', () => { 4 | describe('getExpires', () => { 5 | it('Check if round get expired', () => { 6 | expect(RoundUtils.getExpires(522, 131, 131, 5)).toBeGreaterThanOrEqual(1641564794); 7 | }); 8 | it('Check if epoch is 0', () => { 9 | const now = Math.floor(Date.now() / 1000); 10 | expect(RoundUtils.getExpires(0, 131, 131, 5)).toEqual(now); 11 | }); 12 | }); 13 | }); 14 | 15 | -------------------------------------------------------------------------------- /packages/common/test/utils/string.utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { StringUtils } from "../../src/utils/string.utils"; 2 | 3 | describe('String Utils', () => { 4 | describe('isHex', () => { 5 | it('isHex normal cases', () => { 6 | expect(StringUtils.isHex('00aa')).toBeTruthy(); 7 | expect(StringUtils.isHex('00AA')).toBeTruthy(); 8 | expect(StringUtils.isHex('00ga')).toBeFalsy(); 9 | expect(StringUtils.isHex('00GA')).toBeFalsy(); 10 | }); 11 | }); 12 | describe('isFunctionName', () => { 13 | it('isFunctionName normal cases', () => { 14 | expect(StringUtils.isHex('08xy')).toBeFalsy(); 15 | expect(StringUtils.isHex('08XY')).toBeFalsy(); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/common/test/utils/token.utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { TokenUtils } from "../../src/utils/token.utils"; 2 | 3 | describe('isToken', () => { 4 | it('Check isToken function', () => { 5 | expect(TokenUtils.isToken('MEX-455c57')).toBeTruthy(); 6 | expect(TokenUtils.isToken('EWLD-e23800-455c74')).toBeFalsy(); 7 | }); 8 | }); 9 | 10 | describe('isCollection', () => { 11 | it('Check isCollection function', () => { 12 | expect(TokenUtils.isCollection('MOS-b9b4b2')).toBeTruthy(); 13 | expect(TokenUtils.isCollection('MOS-b9b4b2-455c74')).toBeFalsy(); 14 | }); 15 | }); 16 | 17 | describe('isNft', () => { 18 | it('Check isNft function', () => { 19 | expect(TokenUtils.isNft('MOS-b9b4b2-947a3912')).toBeTruthy(); 20 | expect(TokenUtils.isNft('MOS-b9b4b2')).toBeFalsy(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/common/test/utils/url.utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { UrlUtils } from "../../src/utils/url.utils"; 2 | 3 | describe('isLocalhost', () => { 4 | it('should return true', () => { 5 | expect(UrlUtils.isLocalhost('https://localhost:2000')).toBeTruthy(); 6 | expect(UrlUtils.isLocalhost('http://localhost:2000')).toBeTruthy(); 7 | }); 8 | 9 | it('should return false', () => { 10 | expect(UrlUtils.isLocalhost('multiversx.com')).toBeFalsy(); 11 | expect(UrlUtils.isLocalhost('http://multiversx.com')).toBeFalsy(); 12 | expect(UrlUtils.isLocalhost('https://multiversx.com')).toBeFalsy(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Import non-ES modules as default imports. 4 | "outDir": "./lib", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "es2017", 12 | "sourceMap": true, 13 | "baseUrl": "./", 14 | "incremental": true, 15 | // enable strict null checks as a best practice 16 | "strictNullChecks": true, 17 | // Search under node_modules for non-relative imports. 18 | "moduleResolution": "node", 19 | // Process & infer types from .js files. 20 | "allowJs": true, 21 | // Enable strictest settings like strictNullChecks & noImplicitAny. 22 | "strict": true, 23 | // Import non-ES modules as default imports. 24 | "esModuleInterop": true, 25 | "alwaysStrict": true, 26 | "allowUnreachableCode": false, 27 | "noImplicitAny": true, 28 | "noImplicitThis": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noImplicitReturns": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "forceConsistentCasingInFileNames": true, 34 | "importHelpers": true, 35 | }, 36 | "include": [ 37 | "src", 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } -------------------------------------------------------------------------------- /packages/elastic/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | // 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | rules: { 18 | "@typescript-eslint/no-explicit-any": ["off"], 19 | "@typescript-eslint/no-unused-vars": ["off"], 20 | "@typescript-eslint/ban-ts-comment": ["off"], 21 | "@typescript-eslint/no-empty-function": ["off"], 22 | "@typescript-eslint/ban-types": ["off"], 23 | "@typescript-eslint/no-var-requires": ["off"], 24 | "@typescript-eslint/no-inferrable-types": ["off"], 25 | "require-await": ["error"], 26 | "@typescript-eslint/no-floating-promises": ["error"], 27 | "max-len": ["off"], 28 | "semi": ["error"], 29 | "comma-dangle": ["error", "always-multiline"], 30 | "eol-last": ["error"], 31 | }, 32 | ignorePatterns: ['.eslintrc.js', '*.spec.ts'], 33 | }; -------------------------------------------------------------------------------- /packages/elastic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@multiversx/sdk-nestjs-elastic", 3 | "version": "5.0.0", 4 | "description": "Multiversx SDK Nestjs elastic package", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/**/*" 9 | ], 10 | "scripts": { 11 | "test": "jest --config=../../jest.config.json --passWithNoTests elastic/*", 12 | "build": "tsc", 13 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", 14 | "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/multiversx/mx-sdk-nestjs.git" 19 | }, 20 | "keywords": [ 21 | "multiversx", 22 | "nestjs", 23 | "elastic" 24 | ], 25 | "author": "MultiversX", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@typescript-eslint/eslint-plugin": "^5.12.0", 29 | "@typescript-eslint/parser": "^5.16.0", 30 | "eslint": "^8.9.0", 31 | "typescript": "^4.3.5" 32 | }, 33 | "peerDependencies": { 34 | "@multiversx/sdk-nestjs-http": "^5.0.0", 35 | "@nestjs/common": "^10.x" 36 | }, 37 | "publishConfig": { 38 | "access": "public" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/elastic/src/elastic.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Provider } from "@nestjs/common"; 2 | import { MetricsModule } from "@multiversx/sdk-nestjs-monitoring"; 3 | import { Module } from "@nestjs/common"; 4 | import { ApiModule } from "@multiversx/sdk-nestjs-http"; 5 | import { ElasticService } from "./elastic.service"; 6 | import { ElasticModuleOptions } from "./entities/elastic.module.options"; 7 | import { ElasticModuleAsyncOptions } from "./entities/elastic.module.async.options"; 8 | 9 | @Global() 10 | @Module({}) 11 | export class ElasticModule { 12 | static forRoot(options: ElasticModuleOptions): DynamicModule { 13 | const providers: Provider[] = [ 14 | { 15 | provide: ElasticModuleOptions, 16 | useFactory: () => options, 17 | }, 18 | ElasticService, 19 | ]; 20 | 21 | return { 22 | module: ElasticModule, 23 | imports: [ 24 | ApiModule, 25 | MetricsModule, 26 | ], 27 | providers, 28 | exports: [ 29 | ElasticService, 30 | ], 31 | }; 32 | } 33 | 34 | static forRootAsync(options: ElasticModuleAsyncOptions): DynamicModule { 35 | const providers: Provider[] = [ 36 | { 37 | provide: ElasticModuleOptions, 38 | useFactory: options.useFactory, 39 | inject: options.inject, 40 | }, 41 | ElasticService, 42 | ]; 43 | 44 | const references = []; 45 | if (options.imports) { 46 | for (const ref of options.imports) { 47 | references.push(ref); 48 | } 49 | } 50 | 51 | return { 52 | module: ElasticModule, 53 | imports: [ 54 | ApiModule, 55 | MetricsModule, 56 | ...references, 57 | ], 58 | providers, 59 | exports: [ 60 | ElasticService, 61 | ], 62 | }; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/abstract.query.ts: -------------------------------------------------------------------------------- 1 | export abstract class AbstractQuery { 2 | abstract getQuery(): any; 3 | } 4 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/elastic.module.async.options.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata } from "@nestjs/common"; 2 | import { ElasticModuleOptions } from "./elastic.module.options"; 3 | 4 | export interface ElasticModuleAsyncOptions extends Pick { 5 | useFactory: (...args: any[]) => Promise | ElasticModuleOptions; 6 | inject?: any[]; 7 | } 8 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/elastic.module.options.ts: -------------------------------------------------------------------------------- 1 | export class ElasticModuleOptions { 2 | constructor(init?: Partial) { 3 | Object.assign(this, init); 4 | } 5 | 6 | url: string = ''; 7 | 8 | customValuePrefix?: string; 9 | } 10 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/elastic.pagination.ts: -------------------------------------------------------------------------------- 1 | export class ElasticPagination { 2 | from: number = 0; 3 | size: number = 25; 4 | } 5 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/elastic.sort.order.ts: -------------------------------------------------------------------------------- 1 | export enum ElasticSortOrder { 2 | descending = 'desc', 3 | ascending = 'asc' 4 | } 5 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/elastic.sort.property.ts: -------------------------------------------------------------------------------- 1 | import { ElasticSortOrder } from "./elastic.sort.order"; 2 | 3 | export class ElasticSortProperty { 4 | name: string = ''; 5 | order: ElasticSortOrder | undefined = undefined; 6 | } 7 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/exists.query.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | 3 | export class ExistsQuery extends AbstractQuery { 4 | constructor(private readonly key: string) { 5 | super(); 6 | } 7 | 8 | getQuery(): any { 9 | return { exists: { field: this.key } }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/match.query.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | import { QueryOperator } from "./query.operator"; 3 | 4 | export class MatchQuery extends AbstractQuery { 5 | constructor( 6 | private readonly key: string, 7 | private readonly value: any, 8 | private readonly operator: QueryOperator | undefined = undefined 9 | ) { 10 | super(); 11 | } 12 | 13 | getQuery(): any { 14 | if (!this.operator) { 15 | return { match: { [this.key]: this.value } }; 16 | } 17 | 18 | return { 19 | match: { 20 | [this.key]: { 21 | query: this.value, 22 | operator: this.operator, 23 | }, 24 | }, 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/must.query.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | 3 | export class MustQuery extends AbstractQuery { 4 | constructor( 5 | private readonly queries: AbstractQuery[], 6 | private readonly mustNotQueries: AbstractQuery[] = [] 7 | ) { 8 | super(); 9 | } 10 | 11 | getQuery(): any { 12 | return { 13 | bool: { 14 | must: this.queries.map(query => query.getQuery()), 15 | must_not: this.mustNotQueries.map(query => query.getQuery()), 16 | }, 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/nested.query.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | import { MatchQuery } from "./match.query"; 3 | 4 | export class NestedQuery extends AbstractQuery { 5 | constructor( 6 | private readonly key: string, 7 | private readonly value: MatchQuery[] 8 | ) { 9 | super(); 10 | } 11 | 12 | getQuery(): any { 13 | return { 14 | nested: { 15 | path: this.key, 16 | query: { 17 | bool: { 18 | must: this.value.map((item) => item.getQuery()), 19 | }, 20 | }, 21 | }, 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/nested.should.query.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | 3 | export class NestedShouldQuery extends AbstractQuery { 4 | constructor( 5 | private readonly key: string, 6 | private readonly value: AbstractQuery[] 7 | ) { 8 | super(); 9 | } 10 | 11 | getQuery(): any { 12 | return { 13 | nested: { 14 | path: this.key, 15 | query: { 16 | bool: { 17 | should: this.value.map((item) => item.getQuery()), 18 | }, 19 | }, 20 | }, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/query.condition.options.ts: -------------------------------------------------------------------------------- 1 | export enum QueryConditionOptions { 2 | should = 'should', 3 | must = 'must', 4 | mustNot = 'must_not' 5 | } 6 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/query.condition.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | 3 | export class QueryCondition { 4 | must: AbstractQuery[] = []; 5 | should: AbstractQuery[] = []; 6 | must_not: AbstractQuery[] = []; 7 | } 8 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/query.operator.ts: -------------------------------------------------------------------------------- 1 | export enum QueryOperator { 2 | AND = 'AND', 3 | OR = 'OR' 4 | } 5 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/query.range.ts: -------------------------------------------------------------------------------- 1 | export abstract class QueryRange { 2 | constructor( 3 | readonly key: string, 4 | readonly value: number 5 | ) { } 6 | } 7 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/query.type.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | import { ExistsQuery } from "./exists.query"; 3 | import { MatchQuery } from "./match.query"; 4 | import { MustQuery } from "./must.query"; 5 | import { NestedQuery } from "./nested.query"; 6 | import { QueryOperator } from "./query.operator"; 7 | import { QueryRange } from "./query.range"; 8 | import { RangeQuery } from "./range.query"; 9 | import { ShouldQuery } from "./should.query"; 10 | import { WildcardQuery } from "./wildcard.query"; 11 | import { StringQuery } from "./string.query"; 12 | import { NestedShouldQuery } from "./nested.should.query"; 13 | import { ScriptQuery } from "./script.query"; 14 | 15 | export class QueryType { 16 | static Match = (key: string, value: any | undefined, operator: QueryOperator | undefined = undefined): MatchQuery => { 17 | return new MatchQuery(key, value, operator); 18 | }; 19 | 20 | static Exists = (key: string): ExistsQuery => { 21 | return new ExistsQuery(key); 22 | }; 23 | 24 | static Range = (key: string, ...ranges: QueryRange[]): RangeQuery => { 25 | return new RangeQuery(key, ranges); 26 | }; 27 | 28 | static Wildcard = (key: string, value: string): WildcardQuery => { 29 | return new WildcardQuery(key, value); 30 | }; 31 | 32 | static Nested = (key: string, value: MatchQuery[]): NestedQuery => { 33 | return new NestedQuery(key, value); 34 | }; 35 | 36 | static NestedShould = (key: string, value: AbstractQuery[]): NestedShouldQuery => { 37 | return new NestedShouldQuery(key, value); 38 | }; 39 | 40 | static Should = (queries: AbstractQuery[]): ShouldQuery => { 41 | return new ShouldQuery(queries); 42 | }; 43 | 44 | static Must = (queries: AbstractQuery[], mustNotQueries: AbstractQuery[] = []): MustQuery => { 45 | return new MustQuery(queries, mustNotQueries); 46 | }; 47 | 48 | static String = (key: string | string[], value: any | undefined): StringQuery => { 49 | return new StringQuery(key, value); 50 | }; 51 | 52 | static Script = (source: string): ScriptQuery => { 53 | return new ScriptQuery(source); 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/range.greater.than.or.equal.ts: -------------------------------------------------------------------------------- 1 | import { QueryRange } from "./query.range"; 2 | 3 | export class RangeGreaterThanOrEqual extends QueryRange { 4 | constructor(value: number) { 5 | super('gte', value); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/range.greater.than.ts: -------------------------------------------------------------------------------- 1 | import { QueryRange } from "./query.range"; 2 | 3 | export class RangeGreaterThan extends QueryRange { 4 | constructor(value: number) { 5 | super('gt', value); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/range.lower.than.or.equal.ts: -------------------------------------------------------------------------------- 1 | import { QueryRange } from "./query.range"; 2 | 3 | export class RangeLowerThanOrEqual extends QueryRange { 4 | constructor(value: number) { 5 | super('lte', value); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/range.lower.than.ts: -------------------------------------------------------------------------------- 1 | import { QueryRange } from "./query.range"; 2 | 3 | export class RangeLowerThan extends QueryRange { 4 | constructor(value: number) { 5 | super('lt', value); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/range.query.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | import { QueryRange } from "./query.range"; 3 | 4 | export class RangeQuery extends AbstractQuery { 5 | constructor( 6 | private readonly key: string, 7 | private readonly ranges: QueryRange[], 8 | ) { 9 | super(); 10 | } 11 | 12 | getQuery(): any { 13 | const conditions: Record = {}; 14 | 15 | for (const range of this.ranges) { 16 | conditions[range.key] = range.value.toString(); 17 | } 18 | 19 | return { range: { [this.key]: conditions } }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/script.query.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | 3 | export class ScriptQuery extends AbstractQuery { 4 | constructor( 5 | private readonly source: string | undefined, 6 | ) { 7 | super(); 8 | } 9 | 10 | getQuery(): any { 11 | return { script: { script: { source: this.source, lang: 'painless' } } }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/should.query.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | 3 | export class ShouldQuery extends AbstractQuery { 4 | constructor(private readonly queries: AbstractQuery[]) { 5 | super(); 6 | } 7 | 8 | getQuery(): any { 9 | return { 10 | bool: { 11 | should: this.queries.map(query => query.getQuery()), 12 | }, 13 | }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/string.query.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | 3 | export class StringQuery extends AbstractQuery { 4 | constructor( 5 | private readonly key: string | string[], 6 | private readonly value: number | undefined, 7 | ) { 8 | super(); 9 | } 10 | 11 | getQuery(): any { 12 | if (this.key instanceof Array) { 13 | return { query_string: { query: this.value, fields: this.key } }; 14 | } 15 | 16 | return { query_string: { query: this.value, default_field: this.key } }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/terms.query.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | 3 | export class TermsQuery extends AbstractQuery { 4 | constructor( 5 | private readonly key: string, 6 | private readonly value: string[], 7 | ) { 8 | super(); 9 | } 10 | 11 | getQuery(): any { 12 | return { 13 | [this.key]: this.value, 14 | }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/elastic/src/entities/wildcard.query.ts: -------------------------------------------------------------------------------- 1 | import { AbstractQuery } from "./abstract.query"; 2 | export class WildcardQuery extends AbstractQuery { 3 | constructor( 4 | private readonly key: string, 5 | private readonly value: string 6 | ) { 7 | super(); 8 | } 9 | 10 | getQuery(): any { 11 | return { 12 | wildcard: { 13 | [this.key]: { 14 | value: this.value, 15 | case_insensitive: true, 16 | }, 17 | }, 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/elastic/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './elastic.module'; 3 | export * from './elastic.service'; 4 | export * from './entities/abstract.query'; 5 | export * from './entities/elastic.pagination'; 6 | export * from './entities/elastic.query'; 7 | export * from './entities/elastic.module.options'; 8 | export * from './entities/elastic.module.async.options'; 9 | export * from './entities/elastic.sort.order'; 10 | export * from './entities/elastic.sort.property'; 11 | export * from './entities/exists.query'; 12 | export * from './entities/match.query'; 13 | export * from './entities/must.query'; 14 | export * from './entities/nested.query'; 15 | export * from './entities/nested.should.query'; 16 | export * from './entities/query.condition.options'; 17 | export * from './entities/query.condition'; 18 | export * from './entities/query.operator'; 19 | export * from './entities/query.range'; 20 | export * from './entities/query.type'; 21 | export * from './entities/range.greater.than'; 22 | export * from './entities/range.greater.than.or.equal'; 23 | export * from './entities/range.lower.than'; 24 | export * from './entities/range.lower.than.or.equal'; 25 | export * from './entities/range.query'; 26 | export * from './entities/should.query'; 27 | export * from './entities/terms.query'; 28 | export * from './entities/wildcard.query'; 29 | -------------------------------------------------------------------------------- /packages/elastic/test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-nestjs/6cda0cd7ef838245ac2d4d10280e4dd023ab922b/packages/elastic/test/.gitkeep -------------------------------------------------------------------------------- /packages/elastic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Import non-ES modules as default imports. 4 | "outDir": "./lib", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "es2017", 12 | "sourceMap": true, 13 | "baseUrl": "./", 14 | "incremental": true, 15 | // enable strict null checks as a best practice 16 | "strictNullChecks": true, 17 | // Search under node_modules for non-relative imports. 18 | "moduleResolution": "node", 19 | // Process & infer types from .js files. 20 | "allowJs": true, 21 | // Enable strictest settings like strictNullChecks & noImplicitAny. 22 | "strict": true, 23 | // Import non-ES modules as default imports. 24 | "esModuleInterop": true, 25 | "alwaysStrict": true, 26 | "allowUnreachableCode": false, 27 | "noImplicitAny": true, 28 | "noImplicitThis": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noImplicitReturns": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "forceConsistentCasingInFileNames": true, 34 | "importHelpers": true, 35 | }, 36 | "include": [ 37 | "src" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } -------------------------------------------------------------------------------- /packages/http/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | // 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | rules: { 18 | "@typescript-eslint/no-explicit-any": ["off"], 19 | "@typescript-eslint/no-unused-vars": ["off"], 20 | "@typescript-eslint/ban-ts-comment": ["off"], 21 | "@typescript-eslint/no-empty-function": ["off"], 22 | "@typescript-eslint/ban-types": ["off"], 23 | "@typescript-eslint/no-var-requires": ["off"], 24 | "@typescript-eslint/no-inferrable-types": ["off"], 25 | "require-await": ["error"], 26 | "@typescript-eslint/no-floating-promises": ["error"], 27 | "max-len": ["off"], 28 | "semi": ["error"], 29 | "comma-dangle": ["error", "always-multiline"], 30 | "eol-last": ["error"], 31 | }, 32 | ignorePatterns: ['.eslintrc.js', '*.spec.ts'], 33 | }; -------------------------------------------------------------------------------- /packages/http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@multiversx/sdk-nestjs-http", 3 | "version": "5.0.0", 4 | "description": "Multiversx SDK Nestjs http package", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/**/*" 9 | ], 10 | "scripts": { 11 | "test": "jest --config=../../jest.config.json --passWithNoTests http/*", 12 | "build": "tsc", 13 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", 14 | "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/multiversx/mx-sdk-nestjs.git" 19 | }, 20 | "keywords": [ 21 | "multiversx", 22 | "nestjs", 23 | "http" 24 | ], 25 | "author": "MultiversX", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@types/axios": "^0.14.0", 29 | "@typescript-eslint/eslint-plugin": "^5.12.0", 30 | "@typescript-eslint/parser": "^5.16.0", 31 | "axios-mock-adapter": "^1.20.0", 32 | "eslint": "^8.9.0", 33 | "typescript": "^4.3.5" 34 | }, 35 | "dependencies": { 36 | "@multiversx/sdk-core": "^14.0.0", 37 | "@multiversx/sdk-native-auth-client": "^1.0.9", 38 | "agentkeepalive": "^4.3.0", 39 | "axios": "^1.7.4" 40 | }, 41 | "peerDependencies": { 42 | "@multiversx/sdk-nestjs-common": "^5.0.0", 43 | "@multiversx/sdk-nestjs-monitoring": "^5.0.0", 44 | "@nestjs/common": "^10.x", 45 | "@nestjs/core": "^10.x" 46 | }, 47 | "publishConfig": { 48 | "access": "public" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/http/src/api.utils.ts: -------------------------------------------------------------------------------- 1 | export class ApiUtils { 2 | static mergeObjects(obj1: T, obj2: any) { 3 | for (const key of Object.keys(obj2)) { 4 | // @ts-ignore 5 | if (key in obj1) { 6 | // @ts-ignore 7 | obj1[key] = obj2[key]; 8 | } 9 | } 10 | 11 | return obj1; 12 | } 13 | 14 | static cleanupApiValueRecursively(obj: any) { 15 | if (Array.isArray(obj)) { 16 | for (const item of obj) { 17 | if (item && typeof item === 'object') { 18 | ApiUtils.cleanupApiValueRecursively(item); 19 | } 20 | } 21 | } else if (obj && typeof obj === 'object') { 22 | for (const [key, value] of Object.entries(obj)) { 23 | if (typeof value === 'object' || Array.isArray(value)) { 24 | ApiUtils.cleanupApiValueRecursively(value); 25 | } 26 | 27 | if (value === null || value === '' || value === undefined) { 28 | delete obj[key]; 29 | } 30 | 31 | //TODO: think about whether this is applicable everywhere 32 | if (Array.isArray(value) && value.length === 0) { 33 | delete obj[key]; 34 | } 35 | } 36 | } 37 | 38 | return obj; 39 | } 40 | 41 | static replaceUri(uri: string, pattern: string, replacer: string): string { 42 | if (uri.startsWith(pattern)) { 43 | return uri.replace(pattern, replacer); 44 | } 45 | 46 | return uri; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/http/src/api/api.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, Global, Module, Provider } from "@nestjs/common"; 2 | import { MetricsModule } from "@multiversx/sdk-nestjs-monitoring"; 3 | import { ApiService } from "./api.service"; 4 | import { ApiModuleAsyncOptions } from "./entities/api.module.async.options"; 5 | import { ApiModuleOptions } from "./entities/api.module.options"; 6 | 7 | @Global() 8 | @Module({}) 9 | export class ApiModule { 10 | static forRoot(options: ApiModuleOptions): DynamicModule { 11 | const providers: Provider[] = [ 12 | { 13 | provide: ApiModuleOptions, 14 | useFactory: () => options, 15 | }, 16 | ApiService, 17 | ]; 18 | 19 | return { 20 | module: ApiModule, 21 | imports: [ 22 | MetricsModule, 23 | ], 24 | providers, 25 | exports: [ 26 | ApiService, 27 | ], 28 | }; 29 | } 30 | 31 | static forRootAsync(options: ApiModuleAsyncOptions): DynamicModule { 32 | const providers: Provider[] = [ 33 | { 34 | provide: ApiModuleOptions, 35 | useFactory: options.useFactory, 36 | inject: options.inject, 37 | }, 38 | ApiService, 39 | ]; 40 | 41 | const references = []; 42 | if (options.imports) { 43 | for (const ref of options.imports) { 44 | references.push(ref); 45 | } 46 | } 47 | return { 48 | module: ApiModule, 49 | imports: [ 50 | MetricsModule, ...references, 51 | ], 52 | providers, 53 | exports: [ 54 | ApiService, 55 | ], 56 | }; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/http/src/api/entities/api.module.async.options.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata } from "@nestjs/common"; 2 | import { ApiModuleOptions } from "./api.module.options"; 3 | 4 | export interface ApiModuleAsyncOptions extends Pick { 5 | useFactory: (...args: any[]) => Promise | ApiModuleOptions; 6 | inject?: any[]; 7 | } 8 | -------------------------------------------------------------------------------- /packages/http/src/api/entities/api.module.options.ts: -------------------------------------------------------------------------------- 1 | export class ApiModuleOptions { 2 | constructor(init?: Partial) { 3 | Object.assign(this, init); 4 | } 5 | 6 | useKeepAliveAgent: boolean = true; 7 | 8 | axiosTimeout: number = 61000; 9 | 10 | serverTimeout: number = 60000; 11 | 12 | rateLimiterSecret?: string; 13 | 14 | logConnectionKeepAlive: boolean = false; 15 | 16 | useKeepAliveHeader: boolean = false; 17 | 18 | keepAliveMaxFreeSockets?: number; 19 | 20 | keepAliveFreeSocketTimeout?: number; 21 | } 22 | -------------------------------------------------------------------------------- /packages/http/src/api/entities/api.settings.ts: -------------------------------------------------------------------------------- 1 | import { NativeAuthSigner } from "../../auth/native.auth.signer"; 2 | 3 | export interface ApiSettingsBasicCredentials { 4 | username: string; 5 | password: string; 6 | } 7 | 8 | export class ApiSettings { 9 | timeout?: number; 10 | skipRedirects?: boolean; 11 | responseType?: 'arraybuffer' | 'json'; 12 | headers?: Record; 13 | params?: any; 14 | httpsAgent?: any; 15 | auth?: ApiSettingsBasicCredentials; 16 | nativeAuthSigner?: NativeAuthSigner; 17 | validateStatus?: (status: number) => boolean; 18 | } 19 | -------------------------------------------------------------------------------- /packages/http/src/api/entities/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './api.module.async.options'; 3 | export * from './api.module.options'; 4 | export * from './api.settings'; 5 | -------------------------------------------------------------------------------- /packages/http/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entities'; 2 | export * from './api.module'; 3 | export * from './api.service'; 4 | -------------------------------------------------------------------------------- /packages/http/src/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './native.auth.signer'; 2 | -------------------------------------------------------------------------------- /packages/http/src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './api'; 3 | export * from './auth'; 4 | export * from './interceptors'; 5 | export * from './api.utils'; 6 | -------------------------------------------------------------------------------- /packages/http/src/interceptors/cleanup.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; 2 | import { Observable } from "rxjs"; 3 | import { tap } from 'rxjs/operators'; 4 | import { ApiUtils } from "../api.utils"; 5 | 6 | @Injectable() 7 | export class CleanupInterceptor implements NestInterceptor { 8 | intercept(context: ExecutionContext, next: CallHandler): Observable { 9 | const contextType: string = context.getType(); 10 | 11 | if (!["http", "https"].includes(contextType)) { 12 | return next.handle(); 13 | } 14 | 15 | return next 16 | .handle() 17 | .pipe( 18 | tap(result => ApiUtils.cleanupApiValueRecursively(result)) 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/http/src/interceptors/complexity.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; 2 | import { Observable } from "rxjs"; 3 | import { ComplexityExceededException, ApplyComplexityOptions, DecoratorUtils, ComplexityUtils } from "@multiversx/sdk-nestjs-common"; 4 | 5 | @Injectable() 6 | export class ComplexityInterceptor implements NestInterceptor { 7 | constructor(private readonly complexityThreshold: number = 10000) { } 8 | 9 | intercept(context: ExecutionContext, next: CallHandler): Observable { 10 | const contextType: string = context.getType(); 11 | 12 | if (!["http", "https"].includes(contextType)) { 13 | return next.handle(); 14 | } 15 | 16 | const complexityMetadata = DecoratorUtils.getMethodDecorator(ApplyComplexityOptions, context.getHandler()); 17 | if (!complexityMetadata) { 18 | return next.handle(); 19 | } 20 | 21 | this.handleHttpRequest(complexityMetadata.target, context); 22 | 23 | return next.handle(); 24 | } 25 | 26 | private handleHttpRequest(target: any, context: ExecutionContext) { 27 | const query = context.switchToHttp().getRequest().query; 28 | 29 | const fields: string[] = query.fields?.split(",") ?? []; 30 | 31 | for (const [field, value] of Object.entries(query)) { 32 | if (value === "true") { 33 | // special case for REST "resolvers" like "withScResults" or "withOperations". 34 | fields.push(field); 35 | } 36 | } 37 | 38 | const complexityTree = ComplexityUtils.updateComplexityTree(undefined, target, fields, query.size ?? 25); 39 | 40 | const complexity = complexityTree.getComplexity(); 41 | if (complexity > this.complexityThreshold) { 42 | throw new ComplexityExceededException(complexity, this.complexityThreshold); 43 | } 44 | 45 | context.switchToHttp().getRequest().res.set('X-Request-Complexity', complexity); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/http/src/interceptors/entities/disable.fields.interceptor.on.controller.ts: -------------------------------------------------------------------------------- 1 | import { DecoratorUtils } from "@multiversx/sdk-nestjs-common/lib/utils/decorator.utils"; 2 | 3 | export class DisableFieldsInterceptorOnControllerOptions { } 4 | 5 | export function DisableFieldsInterceptorOnController() { 6 | return DecoratorUtils.registerClassDecorator(DisableFieldsInterceptorOnControllerOptions, DisableFieldsInterceptorOnControllerOptions); 7 | } 8 | -------------------------------------------------------------------------------- /packages/http/src/interceptors/entities/disable.fields.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { DecoratorUtils } from "@multiversx/sdk-nestjs-common/lib/utils/decorator.utils"; 2 | 3 | export class DisableFieldsInterceptorOptions { } 4 | 5 | export const DisableFieldsInterceptor = DecoratorUtils.registerMethodDecorator(DisableFieldsInterceptorOptions); 6 | -------------------------------------------------------------------------------- /packages/http/src/interceptors/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './disable.fields.interceptor'; 2 | export * from './disable.fields.interceptor.on.controller'; 3 | -------------------------------------------------------------------------------- /packages/http/src/interceptors/exclude.fields.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | @Injectable() 6 | export class ExcludeFieldsInterceptor implements NestInterceptor { 7 | intercept(context: ExecutionContext, next: CallHandler): Observable { 8 | const contextType: string = context.getType(); 9 | 10 | if (!["http", "https"].includes(contextType)) { 11 | return next.handle(); 12 | } 13 | 14 | const request = context.getArgByIndex(0); 15 | 16 | return next 17 | .handle() 18 | .pipe( 19 | map((resultRef) => { 20 | if (typeof resultRef !== 'object' || resultRef === null) { 21 | return resultRef; 22 | } 23 | 24 | const result = JSON.parse(JSON.stringify(resultRef)); 25 | const excludeFieldsArgument = request.query.excludeFields; 26 | if (excludeFieldsArgument) { 27 | const excludeFields = Array.isArray(excludeFieldsArgument) ? excludeFieldsArgument : excludeFieldsArgument.split(','); 28 | if (Array.isArray(result)) { 29 | for (const item of result) { 30 | this.transformItem(item, excludeFields); 31 | } 32 | } else { 33 | this.transformItem(result, excludeFields); 34 | } 35 | } 36 | return result; 37 | }) 38 | ); 39 | } 40 | 41 | private transformItem(item: any, excludeFields: string[]) { 42 | for (const key of Object.keys(item)) { 43 | if (excludeFields.includes(key)) { 44 | delete item[key]; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/http/src/interceptors/extract.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; 2 | import { Observable } from "rxjs"; 3 | import { map } from 'rxjs/operators'; 4 | 5 | @Injectable() 6 | export class ExtractInterceptor implements NestInterceptor { 7 | intercept(context: ExecutionContext, next: CallHandler): Observable { 8 | const contextType: string = context.getType(); 9 | 10 | if (!["http", "https"].includes(contextType)) { 11 | return next.handle(); 12 | } 13 | 14 | const request = context.getArgByIndex(0); 15 | 16 | return next 17 | .handle() 18 | .pipe(map(result => { 19 | const extractArgument = request.query.extract; 20 | if (extractArgument) { 21 | return result[extractArgument]; 22 | } 23 | 24 | return result; 25 | }) 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/http/src/interceptors/fields.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { DecoratorUtils } from "@multiversx/sdk-nestjs-common"; 2 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; 3 | import { Observable } from "rxjs"; 4 | import { map } from 'rxjs/operators'; 5 | import { DisableFieldsInterceptorOnControllerOptions, DisableFieldsInterceptorOptions } from "./entities"; 6 | 7 | @Injectable() 8 | export class FieldsInterceptor implements NestInterceptor { 9 | intercept(context: ExecutionContext, next: CallHandler): Observable { 10 | const contextType: string = context.getType(); 11 | 12 | if (!["http", "https"].includes(contextType)) { 13 | return next.handle(); 14 | } 15 | 16 | const disableFieldsInterceptorMethodMetadata = DecoratorUtils.getMethodDecorator(DisableFieldsInterceptorOptions, context.getHandler()); 17 | if (disableFieldsInterceptorMethodMetadata) { 18 | return next.handle(); 19 | } 20 | 21 | const disableFieldsInterceptorClassMetadata = DecoratorUtils.getClassDecorator(DisableFieldsInterceptorOnControllerOptions, context.getClass()); 22 | if (disableFieldsInterceptorClassMetadata) { 23 | return next.handle(); 24 | } 25 | 26 | const request = context.getArgByIndex(0); 27 | 28 | return next 29 | .handle() 30 | .pipe( 31 | map((resultRef) => { 32 | if (typeof resultRef !== 'object') { 33 | return resultRef; 34 | } 35 | 36 | const result = JSON.parse(JSON.stringify(resultRef)); 37 | const fieldsArgument = request.query.fields; 38 | if (fieldsArgument) { 39 | const fields = Array.isArray(fieldsArgument) ? fieldsArgument : fieldsArgument.split(','); 40 | if (Array.isArray(result)) { 41 | for (const item of result) { 42 | this.transformItem(item, fields); 43 | } 44 | } else { 45 | this.transformItem(result, fields); 46 | } 47 | } 48 | return result; 49 | }) 50 | ); 51 | } 52 | 53 | private transformItem(item: any, fields: string[]) { 54 | for (const key of Object.keys(item)) { 55 | if (!fields.includes(key)) { 56 | delete item[key]; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/http/src/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entities'; 2 | export * from './complexity.interceptor'; 3 | export * from './extract.interceptor'; 4 | export * from './fields.interceptor'; 5 | export * from './origin.interceptor'; 6 | export * from './pagination.interceptor'; 7 | export * from './query.check.interceptor'; 8 | export * from './cleanup.interceptor'; 9 | export * from './exclude.fields.interceptor'; 10 | -------------------------------------------------------------------------------- /packages/http/src/interceptors/origin.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; 2 | import { Observable, throwError } from "rxjs"; 3 | import { catchError, tap } from 'rxjs/operators'; 4 | import { ContextTracker } from "@multiversx/sdk-nestjs-common"; 5 | import { randomUUID } from 'crypto'; 6 | 7 | @Injectable() 8 | export class OriginInterceptor implements NestInterceptor { 9 | intercept(context: ExecutionContext, next: CallHandler): Observable { 10 | const contextType: string = context.getType(); 11 | 12 | if (!["http", "https"].includes(contextType)) { 13 | return next.handle(); 14 | } 15 | 16 | const apiFunction = context.getClass().name + '.' + context.getHandler().name; 17 | const request = context.switchToHttp().getRequest(); 18 | const requestId = request.headers['x-request-id'] ?? randomUUID(); 19 | 20 | ContextTracker.assign({ origin: apiFunction, requestId }); 21 | 22 | return next 23 | .handle() 24 | .pipe( 25 | tap(() => { 26 | ContextTracker.unassign(); 27 | 28 | if (!request.res.headersSent) { 29 | request.res.set('X-Request-Id', requestId); 30 | } 31 | }), 32 | catchError(err => { 33 | ContextTracker.unassign(); 34 | 35 | if (!request.res.headersSent) { 36 | request.res.set('X-Request-Id', requestId); 37 | } 38 | 39 | return throwError(() => err); 40 | }) 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/http/src/interceptors/pagination.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, HttpException, HttpStatus, NestInterceptor } from "@nestjs/common"; 2 | import { Observable } from "rxjs"; 3 | 4 | export class PaginationInterceptor implements NestInterceptor { 5 | constructor(private readonly maxSize: number = 10000) { } 6 | 7 | intercept(context: ExecutionContext, next: CallHandler): Observable { 8 | const contextType: string = context.getType(); 9 | 10 | if (!["http", "https"].includes(contextType)) { 11 | return next.handle(); 12 | } 13 | 14 | const request = context.getArgByIndex(0); 15 | 16 | const from: number = parseInt(request.query.from || 0); 17 | const size: number = parseInt(request.query.size || 25); 18 | 19 | if (from + size > this.maxSize) { 20 | throw new HttpException(`Result window is too large, from + size must be less than or equal to: [${this.maxSize}] but was [${from + size}]`, HttpStatus.BAD_REQUEST); 21 | } 22 | 23 | return next.handle(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/http/src/interceptors/query.check.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; 2 | import { HttpAdapterHost } from "@nestjs/core"; 3 | import { Observable } from "rxjs"; 4 | 5 | @Injectable() 6 | export class QueryCheckInterceptor implements NestInterceptor { 7 | constructor( 8 | private readonly httpAdapterHost: HttpAdapterHost, 9 | ) { } 10 | 11 | intercept(context: ExecutionContext, next: CallHandler): Observable { 12 | const contextType: string = context.getType(); 13 | 14 | if (!["http", "https"].includes(contextType)) { 15 | return next.handle(); 16 | } 17 | 18 | const httpAdapter = this.httpAdapterHost.httpAdapter; 19 | 20 | const request = context.switchToHttp().getRequest(); 21 | if (httpAdapter.getRequestMethod(request) !== 'GET') { 22 | return next.handle(); 23 | } 24 | 25 | const metadata = Reflect.getOwnMetadata('__routeArguments__', context.getClass(), context.getHandler().name); 26 | if (!metadata) { 27 | return next.handle(); 28 | } 29 | 30 | const supportedQueryNames = Object.values(metadata).map((x: any) => x.data); 31 | 32 | for (const paramName of Object.keys(request.query)) { 33 | if (!['fields', 'extract', 'excludeFields'].includes(paramName) && !supportedQueryNames.includes(paramName)) { 34 | delete request.query[paramName]; 35 | // throw new BadRequestException(`Unsupported parameter '${paramName}'. Supported parameters are: ${supportedQueryNames.join(', ')}`); 36 | // const origin = request.headers['origin']; 37 | // const apiFunction = context.getClass().name + '.' + context.getHandler().name; 38 | // const logger = new Logger(QueryCheckInterceptor.name); 39 | // logger.error(`Unsupported parameter '${paramName}' for function '${apiFunction}', origin '${origin}', ip '${request.clientIp}'`); 40 | } 41 | } 42 | 43 | // rebuild sanitized url for guest caching 44 | const queryParams = new URLSearchParams(request.query).toString(); 45 | const queryParamsUrl = queryParams ? `?${queryParams}` : ''; 46 | request.guestCacheUrl = `${request.baseUrl}${request.path}${queryParamsUrl}`; 47 | 48 | return next.handle(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/http/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Import non-ES modules as default imports. 4 | "outDir": "./lib", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "es2017", 12 | "sourceMap": true, 13 | "baseUrl": "./", 14 | "incremental": true, 15 | // enable strict null checks as a best practice 16 | "strictNullChecks": true, 17 | // Search under node_modules for non-relative imports. 18 | "moduleResolution": "node", 19 | // Process & infer types from .js files. 20 | "allowJs": true, 21 | // Enable strictest settings like strictNullChecks & noImplicitAny. 22 | "strict": true, 23 | // Import non-ES modules as default imports. 24 | "esModuleInterop": true, 25 | "alwaysStrict": true, 26 | "allowUnreachableCode": false, 27 | "noImplicitAny": true, 28 | "noImplicitThis": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noImplicitReturns": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "forceConsistentCasingInFileNames": true, 34 | "importHelpers": true, 35 | }, 36 | "include": [ 37 | "src" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } -------------------------------------------------------------------------------- /packages/monitoring/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | // 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | rules: { 18 | "@typescript-eslint/no-explicit-any": ["off"], 19 | "@typescript-eslint/no-unused-vars": ["off"], 20 | "@typescript-eslint/ban-ts-comment": ["off"], 21 | "@typescript-eslint/no-empty-function": ["off"], 22 | "@typescript-eslint/ban-types": ["off"], 23 | "@typescript-eslint/no-var-requires": ["off"], 24 | "@typescript-eslint/no-inferrable-types": ["off"], 25 | "require-await": ["error"], 26 | "@typescript-eslint/no-floating-promises": ["error"], 27 | "max-len": ["off"], 28 | "semi": ["error"], 29 | "comma-dangle": ["error", "always-multiline"], 30 | "eol-last": ["error"], 31 | }, 32 | ignorePatterns: ['.eslintrc.js', '*.spec.ts'], 33 | }; -------------------------------------------------------------------------------- /packages/monitoring/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@multiversx/sdk-nestjs-monitoring", 3 | "version": "5.0.0", 4 | "description": "Multiversx SDK Nestjs monitoring package", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/**/*" 9 | ], 10 | "scripts": { 11 | "test": "jest --config=../../jest.config.json --passWithNoTests monitoring/*", 12 | "build": "tsc", 13 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", 14 | "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/multiversx/mx-sdk-nestjs.git" 19 | }, 20 | "keywords": [ 21 | "multiversx", 22 | "nestjs", 23 | "monitoring" 24 | ], 25 | "author": "MultiversX", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@typescript-eslint/eslint-plugin": "^5.12.0", 29 | "@typescript-eslint/parser": "^5.16.0", 30 | "eslint": "^8.9.0", 31 | "typescript": "^4.3.5" 32 | }, 33 | "dependencies": { 34 | "prom-client": "^14.0.1", 35 | "winston": "^3.7.2", 36 | "winston-daily-rotate-file": "^4.6.1" 37 | }, 38 | "peerDependencies": { 39 | "@nestjs/common": "^10.x" 40 | }, 41 | "publishConfig": { 42 | "access": "public" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/monitoring/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './metrics'; 2 | export * from './profilers'; 3 | export * from './interceptors'; 4 | -------------------------------------------------------------------------------- /packages/monitoring/src/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './log.requests.interceptor'; 2 | export * from './logging.interceptor'; 3 | export * from './request.cpu.time.interceptor'; 4 | -------------------------------------------------------------------------------- /packages/monitoring/src/interceptors/log.requests.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; 2 | import { HttpAdapterHost } from "@nestjs/core"; 3 | import { Observable } from "rxjs"; 4 | import winston from "winston"; 5 | import DailyRotateFile from "winston-daily-rotate-file"; 6 | 7 | @Injectable() 8 | export class LogRequestsInterceptor implements NestInterceptor { 9 | private readonly logger: winston.Logger; 10 | 11 | constructor( 12 | private readonly httpAdapterHost: HttpAdapterHost, 13 | ) { 14 | this.logger = winston.createLogger({ 15 | transports: [ 16 | new DailyRotateFile({ 17 | filename: 'requests-%DATE%.log', 18 | datePattern: 'YYYY-MM-DD', 19 | zippedArchive: true, 20 | maxSize: '100m', 21 | maxFiles: '14d', 22 | dirname: 'dist/logs', 23 | format: winston.format.combine( 24 | winston.format.timestamp(), 25 | winston.format.json(), 26 | ), 27 | }), 28 | ], 29 | }); 30 | } 31 | 32 | intercept(context: ExecutionContext, next: CallHandler): Observable { 33 | const contextType: string = context.getType(); 34 | 35 | if (!["http", "https"].includes(contextType)) { 36 | return next.handle(); 37 | } 38 | 39 | const httpAdapter = this.httpAdapterHost.httpAdapter; 40 | 41 | const request = context.getArgByIndex(0); 42 | if (httpAdapter.getRequestMethod(request) !== 'GET') { 43 | return next.handle(); 44 | } 45 | 46 | const url = httpAdapter.getRequestUrl(request); 47 | 48 | this.logger.info(url); 49 | 50 | return next.handle(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/monitoring/src/interceptors/logging.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, HttpStatus, Injectable, NestInterceptor } from "@nestjs/common"; 2 | import { Observable, throwError } from "rxjs"; 3 | import { catchError, tap } from "rxjs/operators"; 4 | import { MetricsService } from "../metrics"; 5 | import { PerformanceProfiler } from "../profilers/performance.profiler"; 6 | 7 | @Injectable() 8 | export class LoggingInterceptor implements NestInterceptor { 9 | constructor( 10 | private readonly metricsService: MetricsService, 11 | ) { } 12 | 13 | intercept(context: ExecutionContext, next: CallHandler): Observable { 14 | const contextType: string = context.getType(); 15 | 16 | if (!["http", "https"].includes(contextType)) { 17 | return next.handle(); 18 | } 19 | 20 | const apiFunction = context.getClass().name + '.' + context.getHandler().name; 21 | 22 | const profiler = new PerformanceProfiler(apiFunction); 23 | 24 | const request = context.getArgByIndex(0); 25 | 26 | let origin = request.headers['origin']; 27 | if (!origin) { 28 | origin = 'Unknown'; 29 | } 30 | 31 | return next 32 | .handle() 33 | .pipe( 34 | tap(() => { 35 | profiler.stop(); 36 | 37 | const http = context.switchToHttp(); 38 | const res = http.getResponse(); 39 | 40 | this.metricsService.setApiCall(apiFunction, origin, res.statusCode, profiler.duration); 41 | }), 42 | catchError(err => { 43 | profiler.stop(); 44 | 45 | const statusCode = err.status ?? HttpStatus.INTERNAL_SERVER_ERROR; 46 | this.metricsService.setApiCall(apiFunction, origin, statusCode, profiler.duration); 47 | 48 | return throwError(() => err); 49 | }) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/monitoring/src/interceptors/request.cpu.time.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common"; 2 | import { Observable, throwError } from "rxjs"; 3 | import { catchError, tap } from 'rxjs/operators'; 4 | import { MetricsService } from '../metrics'; 5 | import { CpuProfiler } from "../profilers/cpu.profiler"; 6 | 7 | @Injectable() 8 | export class RequestCpuTimeInterceptor implements NestInterceptor { 9 | constructor( 10 | private readonly metricsService: MetricsService 11 | ) { } 12 | 13 | intercept(context: ExecutionContext, next: CallHandler): Observable { 14 | const contextType: string = context.getType(); 15 | 16 | if (!["http", "https"].includes(contextType)) { 17 | return next.handle(); 18 | } 19 | 20 | const apiFunction = context.getClass().name + '.' + context.getHandler().name; 21 | const request = context.switchToHttp().getRequest(); 22 | 23 | const profiler = new CpuProfiler(); 24 | 25 | return next 26 | .handle() 27 | .pipe( 28 | tap(() => { 29 | const duration = profiler.stop(); 30 | this.metricsService.setApiCpuTime(apiFunction, duration); 31 | 32 | if (!request.res.headersSent) { 33 | request.res.set('X-Request-Cpu-Time', duration); 34 | } 35 | }), 36 | catchError(err => { 37 | const duration = profiler.stop(); 38 | this.metricsService.setApiCpuTime(apiFunction, duration); 39 | 40 | if (!request.res.headersSent) { 41 | request.res.set('X-Request-Cpu-Time', duration); 42 | } 43 | 44 | return throwError(() => err); 45 | }) 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/monitoring/src/metrics/entities/elastic.metric.type.ts: -------------------------------------------------------------------------------- 1 | export enum ElasticMetricType { 2 | list = 'list', 3 | item = 'item', 4 | count = 'count' 5 | } 6 | -------------------------------------------------------------------------------- /packages/monitoring/src/metrics/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entities/elastic.metric.type'; 2 | export * from './metrics.module'; 3 | export * from './metrics.service'; 4 | -------------------------------------------------------------------------------- /packages/monitoring/src/metrics/metrics.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from "@nestjs/common"; 2 | import { MetricsService } from "./metrics.service"; 3 | 4 | @Global() 5 | @Module({ 6 | providers: [ 7 | MetricsService, 8 | ], 9 | exports: [ 10 | MetricsService, 11 | ], 12 | }) 13 | export class MetricsModule { } 14 | -------------------------------------------------------------------------------- /packages/monitoring/src/profilers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cpu.profiler'; 2 | export * from './performance.profiler'; 3 | -------------------------------------------------------------------------------- /packages/monitoring/src/profilers/performance.profiler.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from "@nestjs/common"; 2 | 3 | export class PerformanceProfiler { 4 | started: number; 5 | description: string; 6 | 7 | stopped: number = 0; 8 | duration: number = 0; 9 | 10 | constructor(description: string = '') { 11 | this.started = this.now(); 12 | this.description = description; 13 | } 14 | 15 | stop(description: string | null = null, log: boolean = false): number { 16 | this.stopped = this.now(); 17 | this.duration = this.stopped - this.started; 18 | 19 | if (log) { 20 | const logger = new Logger(PerformanceProfiler.name); 21 | 22 | logger.log(`${description ?? this.description}: ${this.duration.toFixed(3)}ms`); 23 | } 24 | 25 | return this.duration; 26 | } 27 | 28 | peek(): number { 29 | return this.now() - this.started; 30 | } 31 | 32 | private now() { 33 | const hrTime = process.hrtime(); 34 | return hrTime[0] * 1000 + hrTime[1] / 1000000; 35 | } 36 | 37 | static async profile(description: string, promise: Promise | (() => Promise)): Promise { 38 | const profiler = new PerformanceProfiler(); 39 | 40 | try { 41 | if (promise instanceof Function) { 42 | return await promise(); 43 | } else { 44 | return await promise; 45 | } 46 | } finally { 47 | profiler.stop(description, true); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/monitoring/test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-nestjs/6cda0cd7ef838245ac2d4d10280e4dd023ab922b/packages/monitoring/test/.gitkeep -------------------------------------------------------------------------------- /packages/monitoring/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Import non-ES modules as default imports. 4 | "outDir": "./lib", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "es2017", 12 | "sourceMap": true, 13 | "baseUrl": "./", 14 | "incremental": true, 15 | // enable strict null checks as a best practice 16 | "strictNullChecks": true, 17 | // Search under node_modules for non-relative imports. 18 | "moduleResolution": "node", 19 | // Process & infer types from .js files. 20 | "allowJs": true, 21 | // Enable strictest settings like strictNullChecks & noImplicitAny. 22 | "strict": true, 23 | // Import non-ES modules as default imports. 24 | "esModuleInterop": true, 25 | "alwaysStrict": true, 26 | "allowUnreachableCode": false, 27 | "noImplicitAny": true, 28 | "noImplicitThis": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noImplicitReturns": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "forceConsistentCasingInFileNames": true, 34 | "importHelpers": true, 35 | }, 36 | "include": [ 37 | "src" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } -------------------------------------------------------------------------------- /packages/rabbitmq/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | // 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | rules: { 18 | "@typescript-eslint/no-explicit-any": ["off"], 19 | "@typescript-eslint/no-unused-vars": ["off"], 20 | "@typescript-eslint/ban-ts-comment": ["off"], 21 | "@typescript-eslint/no-empty-function": ["off"], 22 | "@typescript-eslint/ban-types": ["off"], 23 | "@typescript-eslint/no-var-requires": ["off"], 24 | "@typescript-eslint/no-inferrable-types": ["off"], 25 | "require-await": ["error"], 26 | "@typescript-eslint/no-floating-promises": ["error"], 27 | "max-len": ["off"], 28 | "semi": ["error"], 29 | "comma-dangle": ["error", "always-multiline"], 30 | "eol-last": ["error"], 31 | }, 32 | ignorePatterns: ['.eslintrc.js', '*.spec.ts'], 33 | }; -------------------------------------------------------------------------------- /packages/rabbitmq/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@multiversx/sdk-nestjs-rabbitmq", 3 | "version": "5.0.0", 4 | "description": "Multiversx SDK Nestjs rabbitmq client package", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/**/*" 9 | ], 10 | "scripts": { 11 | "test": "jest --config=../../jest.config.json --passWithNoTests rabbitmq/*", 12 | "build": "tsc", 13 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", 14 | "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/multiversx/mx-sdk-nestjs.git" 19 | }, 20 | "keywords": [ 21 | "multiversx", 22 | "nestjs", 23 | "rabbitmq" 24 | ], 25 | "author": "MultiversX", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@types/amqplib": "^0.8.2", 29 | "@types/uuid": "^8.3.4", 30 | "@typescript-eslint/eslint-plugin": "^5.12.0", 31 | "@typescript-eslint/parser": "^5.16.0", 32 | "eslint": "^8.9.0", 33 | "typescript": "^4.3.5" 34 | }, 35 | "dependencies": { 36 | "@golevelup/nestjs-rabbitmq": "4.0.0", 37 | "uuid": "^8.3.2" 38 | }, 39 | "peerDependencies": { 40 | "@multiversx/sdk-nestjs-common": "^5.0.0", 41 | "@nestjs/common": "^10.x" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/rabbitmq/src/entities/async-options.interface.ts: -------------------------------------------------------------------------------- 1 | import { ModuleMetadata } from '@nestjs/common'; 2 | import { RabbitModuleOptions } from './options'; 3 | 4 | export interface RabbitModuleAsyncOptions extends Pick { 5 | useFactory: (...args: any[]) => Promise | RabbitModuleOptions; 6 | inject?: any[]; 7 | } 8 | -------------------------------------------------------------------------------- /packages/rabbitmq/src/entities/constants.ts: -------------------------------------------------------------------------------- 1 | export const RABBIT_ADDITIONAL_OPTIONS = 'RABBIT_ADDITIONAL_OPTIONS'; 2 | -------------------------------------------------------------------------------- /packages/rabbitmq/src/entities/consumer-config.interface.ts: -------------------------------------------------------------------------------- 1 | export interface RabbitConsumerConfig { 2 | exchange: string; 3 | disable?: boolean; 4 | queue: string; 5 | } 6 | -------------------------------------------------------------------------------- /packages/rabbitmq/src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './async-options.interface'; 2 | export * from './constants'; 3 | export * from './consumer-config.interface'; 4 | export * from './options.interface'; 5 | export * from './options'; 6 | -------------------------------------------------------------------------------- /packages/rabbitmq/src/entities/options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface OptionsInterface { 2 | logsVerbose?: boolean; 3 | checkForDuplicates?: boolean; 4 | duplicatesCheckTtl?: number; 5 | } 6 | -------------------------------------------------------------------------------- /packages/rabbitmq/src/entities/options.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionInitOptions, RabbitMQExchangeConfig } from '@golevelup/nestjs-rabbitmq'; 2 | 3 | export class RabbitModuleOptions { 4 | uri: string = ''; 5 | connectionInitOptions?: ConnectionInitOptions | undefined; 6 | exchanges?: RabbitMQExchangeConfig[] | undefined; 7 | prefetchCount?: number | undefined; 8 | 9 | constructor( 10 | uri: string, 11 | exchanges: string[] | undefined = undefined, 12 | connectionInitOptions: ConnectionInitOptions | undefined = undefined, 13 | prefetchCount?: number | undefined, 14 | ) { 15 | this.uri = uri; 16 | this.exchanges = this.getExchanges(exchanges); 17 | this.connectionInitOptions = connectionInitOptions; 18 | this.prefetchCount = prefetchCount; 19 | } 20 | 21 | private getExchanges( 22 | exchanges: string[] | undefined, 23 | ): RabbitMQExchangeConfig[] | undefined { 24 | if (!exchanges) { 25 | return; 26 | } 27 | 28 | return exchanges.map(exchange => { 29 | return { 30 | name: exchange, 31 | type: 'fanout', 32 | options: {}, 33 | }; 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/rabbitmq/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entities/consumer-config.interface'; 2 | export * from './rabbit.module'; 3 | export * from './publisher.service'; 4 | export * from './subscribers.decorators'; 5 | export * from './entities/options'; 6 | export * from './entities/async-options.interface'; 7 | export * from './rabbit-context-checker.service'; 8 | export * from './interceptors/rabbitmq-consumer-monitoring.interceptor'; 9 | -------------------------------------------------------------------------------- /packages/rabbitmq/src/interceptors/rabbitmq-consumer-monitoring.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, ExecutionContext, Injectable, NestInterceptor, 3 | } from '@nestjs/common'; 4 | import { Observable, throwError } from 'rxjs'; 5 | import { catchError, tap } from 'rxjs/operators'; 6 | import { isRabbitContext } from '@golevelup/nestjs-rabbitmq'; 7 | import { PerformanceProfiler, MetricsService } from '@multiversx/sdk-nestjs-monitoring'; 8 | 9 | @Injectable() 10 | export class RabbitMqConsumerMonitoringInterceptor implements NestInterceptor { 11 | constructor( 12 | private readonly metricsService: MetricsService, 13 | ) { } 14 | 15 | intercept( 16 | context: ExecutionContext, 17 | next: CallHandler, 18 | ): Observable { 19 | const isRmqContext = isRabbitContext(context); 20 | if (!isRmqContext) { 21 | return next.handle(); 22 | } 23 | 24 | const consumer = context.getClass().name; 25 | 26 | const profiler = new PerformanceProfiler(); 27 | 28 | return next 29 | .handle() 30 | .pipe( 31 | tap(() => { 32 | this.metricsService.setConsumer(consumer, profiler.stop()); 33 | }), 34 | catchError(err => { 35 | this.metricsService.setConsumer(consumer, profiler.stop()); 36 | 37 | return throwError(() => err); 38 | }), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/rabbitmq/src/rabbit-context-checker.service.ts: -------------------------------------------------------------------------------- 1 | import { isRabbitContext } from '@golevelup/nestjs-rabbitmq'; 2 | import { ExecutionContext, Injectable } from '@nestjs/common'; 3 | 4 | @Injectable() 5 | export class RabbitContextCheckerService { 6 | check(context: ExecutionContext): boolean { 7 | return isRabbitContext(context); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/rabbitmq/test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-nestjs/6cda0cd7ef838245ac2d4d10280e4dd023ab922b/packages/rabbitmq/test/.gitkeep -------------------------------------------------------------------------------- /packages/rabbitmq/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Import non-ES modules as default imports. 4 | "outDir": "./lib", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "es2017", 12 | "sourceMap": true, 13 | "baseUrl": "./", 14 | "incremental": true, 15 | // enable strict null checks as a best practice 16 | "strictNullChecks": true, 17 | // Search under node_modules for non-relative imports. 18 | "moduleResolution": "node", 19 | // Process & infer types from .js files. 20 | "allowJs": true, 21 | // Enable strictest settings like strictNullChecks & noImplicitAny. 22 | "strict": true, 23 | // Import non-ES modules as default imports. 24 | "esModuleInterop": true, 25 | "alwaysStrict": true, 26 | "allowUnreachableCode": false, 27 | "noImplicitAny": true, 28 | "noImplicitThis": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noImplicitReturns": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "forceConsistentCasingInFileNames": true, 34 | "importHelpers": true, 35 | }, 36 | "include": [ 37 | "src" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } -------------------------------------------------------------------------------- /packages/redis/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | // 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | rules: { 18 | "@typescript-eslint/no-explicit-any": ["off"], 19 | "@typescript-eslint/no-unused-vars": ["off"], 20 | "@typescript-eslint/ban-ts-comment": ["off"], 21 | "@typescript-eslint/no-empty-function": ["off"], 22 | "@typescript-eslint/ban-types": ["off"], 23 | "@typescript-eslint/no-var-requires": ["off"], 24 | "@typescript-eslint/no-inferrable-types": ["off"], 25 | "require-await": ["error"], 26 | "@typescript-eslint/no-floating-promises": ["error"], 27 | "max-len": ["off"], 28 | "semi": ["error"], 29 | "comma-dangle": ["error", "always-multiline"], 30 | "eol-last": ["error"], 31 | }, 32 | ignorePatterns: ['.eslintrc.js', '*.spec.ts'], 33 | }; -------------------------------------------------------------------------------- /packages/redis/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@multiversx/sdk-nestjs-redis", 3 | "version": "5.0.0", 4 | "description": "Multiversx SDK Nestjs redis client package", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib/**/*" 9 | ], 10 | "scripts": { 11 | "test": "jest --config=../../jest.config.json --passWithNoTests redis/*", 12 | "build": "tsc", 13 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", 14 | "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/multiversx/mx-sdk-nestjs.git" 19 | }, 20 | "keywords": [ 21 | "multiversx", 22 | "nestjs", 23 | "redis" 24 | ], 25 | "author": "MultiversX", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "@types/redis": "^2.8.32", 29 | "@typescript-eslint/eslint-plugin": "^5.12.0", 30 | "@typescript-eslint/parser": "^5.16.0", 31 | "eslint": "^8.9.0", 32 | "typescript": "^4.3.5" 33 | }, 34 | "dependencies": { 35 | "ioredis": "^5.2.3" 36 | }, 37 | "peerDependencies": { 38 | "@nestjs/common": "^10.x" 39 | }, 40 | "publishConfig": { 41 | "access": "public" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/redis/src/entities/common.constants.ts: -------------------------------------------------------------------------------- 1 | export const REDIS_CLIENT_TOKEN = 'REDIS_CLIENT_TOKEN'; 2 | export const REDIS_OPTIONS_TOKEN = 'REDIS_OPTIONS_TOKEN'; 3 | export const REDIS_CURRENT_CLIENT_TOKEN = 'REDIS_CURRENT_CLIENT_TOKEN'; 4 | -------------------------------------------------------------------------------- /packages/redis/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './entities/common.constants'; 2 | export * from './options'; 3 | export * from './redis.module'; 4 | -------------------------------------------------------------------------------- /packages/redis/src/options.ts: -------------------------------------------------------------------------------- 1 | import { RedisOptions } from 'ioredis'; 2 | import { ModuleMetadata, Type } from '@nestjs/common/interfaces'; 3 | 4 | export type RedisDefaultOptions = RedisOptions; 5 | 6 | export interface RedisModuleOptions { 7 | config: RedisOptions; 8 | } 9 | 10 | export interface RedisModuleOptionsFactory { 11 | createRedisModuleOptions(): Promise | RedisModuleOptions; 12 | } 13 | 14 | export interface RedisModuleAsyncOptions extends Pick { 15 | inject?: any[]; 16 | useClass?: Type; 17 | useExisting?: Type; 18 | useFactory?: (...args: any[]) => Promise | RedisModuleOptions; 19 | } 20 | -------------------------------------------------------------------------------- /packages/redis/test/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-nestjs/6cda0cd7ef838245ac2d4d10280e4dd023ab922b/packages/redis/test/.gitkeep -------------------------------------------------------------------------------- /packages/redis/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Import non-ES modules as default imports. 4 | "outDir": "./lib", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "es2017", 12 | "sourceMap": true, 13 | "baseUrl": "./", 14 | "incremental": true, 15 | // enable strict null checks as a best practice 16 | "strictNullChecks": true, 17 | // Search under node_modules for non-relative imports. 18 | "moduleResolution": "node", 19 | // Process & infer types from .js files. 20 | "allowJs": true, 21 | // Enable strictest settings like strictNullChecks & noImplicitAny. 22 | "strict": true, 23 | // Import non-ES modules as default imports. 24 | "esModuleInterop": true, 25 | "alwaysStrict": true, 26 | "allowUnreachableCode": false, 27 | "noImplicitAny": true, 28 | "noImplicitThis": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noImplicitReturns": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "forceConsistentCasingInFileNames": true, 34 | "importHelpers": true, 35 | }, 36 | "include": [ 37 | "src" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Import non-ES modules as default imports. 4 | "outDir": "./lib", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "removeComments": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "allowSyntheticDefaultImports": true, 11 | "target": "es2017", 12 | "sourceMap": true, 13 | "baseUrl": "./", 14 | "incremental": true, 15 | // enable strict null checks as a best practice 16 | "strictNullChecks": true, 17 | // Search under node_modules for non-relative imports. 18 | "moduleResolution": "node", 19 | // Process & infer types from .js files. 20 | "allowJs": true, 21 | // Enable strictest settings like strictNullChecks & noImplicitAny. 22 | "strict": true, 23 | // Import non-ES modules as default imports. 24 | "esModuleInterop": true, 25 | "alwaysStrict": true, 26 | "allowUnreachableCode": false, 27 | "noImplicitAny": true, 28 | "noImplicitThis": true, 29 | "noUnusedLocals": true, 30 | "noUnusedParameters": true, 31 | "noImplicitReturns": true, 32 | "noFallthroughCasesInSwitch": true, 33 | "forceConsistentCasingInFileNames": true, 34 | "importHelpers": true, 35 | }, 36 | "include": [ 37 | "packages" 38 | ], 39 | "exclude": [ 40 | "node_modules", 41 | "**/__tests__/*" 42 | ] 43 | } --------------------------------------------------------------------------------