├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .swcrc ├── .vscode └── extensions.json ├── README.md ├── jest.config.ts ├── jest.preset.js ├── nx.json ├── package.json ├── packages ├── .gitkeep ├── fastify-multer │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── project.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── fastify-multer-core.module.ts │ │ │ ├── fastify-multer.module-definition.ts │ │ │ ├── fastify-multer.module.ts │ │ │ ├── files.constants.ts │ │ │ ├── interceptors │ │ │ ├── any-files.interceptor.ts │ │ │ ├── file-fields.interceptor.ts │ │ │ ├── file.interceptor.ts │ │ │ ├── files.interceptor.ts │ │ │ ├── index.ts │ │ │ └── no-files.interceptor.ts │ │ │ ├── interfaces │ │ │ ├── file-uploads.interface.ts │ │ │ ├── index.ts │ │ │ └── multer-options.interface.ts │ │ │ └── multer │ │ │ ├── multer.constants.ts │ │ │ └── multer.utils.ts │ ├── test │ │ ├── file-upload │ │ │ ├── app │ │ │ │ ├── app.controller.ts │ │ │ │ └── app.module.ts │ │ │ └── file-upload.spec.ts │ │ ├── index.spec.ts │ │ ├── multiple-imports │ │ │ ├── app │ │ │ │ └── app.module.ts │ │ │ ├── bar │ │ │ │ └── bar.module.ts │ │ │ ├── foo │ │ │ │ └── foo.module.ts │ │ │ └── multiple-imports.spec.ts │ │ └── uploads-from-options │ │ │ ├── app │ │ │ ├── app.controller.ts │ │ │ └── app.module.ts │ │ │ └── upload-from-options.spec.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── or-guard │ ├── .babelrc │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── and.guard.ts │ │ │ └── or.guard.ts │ ├── test │ │ ├── app.controller.ts │ │ ├── app.module.ts │ │ ├── obs.guard.ts │ │ ├── or.guard.spec.ts │ │ ├── prom.guard.ts │ │ ├── read-user.guard.ts │ │ ├── set-user.guard.ts │ │ ├── sync.guard.ts │ │ └── throw.guard.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── throttler-storage-redis │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── docker-compose.yml │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ ├── index.ts │ │ ├── throttler-storage-redis.interface.ts │ │ ├── throttler-storage-redis.service.ts │ │ └── type.ts │ ├── test │ │ ├── app │ │ │ ├── app.module.ts │ │ │ ├── app.service.ts │ │ │ ├── controllers │ │ │ │ ├── app.controller.ts │ │ │ │ ├── cluster-controller.module.ts │ │ │ │ ├── controller.module.ts │ │ │ │ ├── default.controller.ts │ │ │ │ └── limit.controller.ts │ │ │ └── main.ts │ │ ├── controller.spec.ts │ │ ├── jest-e2e.json │ │ └── utility │ │ │ ├── httpromise.ts │ │ │ ├── redis-cluster.ts │ │ │ └── redis.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json └── typeschema │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ ├── index.ts │ └── lib │ │ ├── index.ts │ │ ├── typeschema-options.interface.ts │ │ ├── typeschema.constants.ts │ │ ├── typeschema.dto.ts │ │ └── typeschema.pipe.ts │ ├── test │ ├── app.controller.ts │ ├── app.spec.ts │ └── models │ │ ├── ajv.dto.ts │ │ ├── arktype.dto.ts │ │ ├── common.ts │ │ ├── index.ts │ │ ├── io-ts.dto.ts │ │ ├── joi.dto.ts │ │ ├── ow.dto.ts │ │ ├── runtypes.dto.ts │ │ ├── superstruct.dto.ts │ │ ├── typebox.dto.ts │ │ ├── valibot.dto.ts │ │ ├── yup.dto.ts │ │ └── zod.dto.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.spec.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tools └── tsconfig.tools.json ├── tsconfig.base.json └── vitest.workspace.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ], 22 | "@typescript-eslint/no-unused-vars": [ 23 | "error", 24 | { 25 | "varsIgnorePattern": "^_.*", 26 | "argsIgnorePattern": "^_.*" 27 | } 28 | ] 29 | } 30 | }, 31 | { 32 | "files": ["*.ts", "*.tsx"], 33 | "extends": ["plugin:@nx/typescript"], 34 | "rules": { 35 | "@typescript-eslint/no-extra-semi": "error", 36 | "no-extra-semi": "off" 37 | } 38 | }, 39 | { 40 | "files": ["*.js", "*.jsx"], 41 | "extends": ["plugin:@nx/javascript"], 42 | "rules": { 43 | "@typescript-eslint/no-extra-semi": "error", 44 | "no-extra-semi": "off" 45 | } 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | env: 9 | NX_CLOUD_DISTRIBUTED_EXECUTION: ${{ !contains(github.event.pull_request.user.login, 'dependabot') }} 10 | NX_CLOUD_AUTH_TOKEN: ${{ secrets.NX_CLOUD_TOKEN }} 11 | 12 | jobs: 13 | main: 14 | runs-on: ubuntu-latest 15 | if: ${{ github.event_name != 'pull_request' }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | name: Checkout [main] 19 | with: 20 | fetch-depth: 0 21 | - name: Derive appropriate SHAs for base and head for `nx affected` commands 22 | uses: nrwl/nx-set-shas@v1 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: 18.x 26 | - name: Install pnpm 27 | run: npm i -g pnpm 28 | - name: Install deps 29 | run: pnpm i --frozen-lockfile=false 30 | - name: Build Projects 31 | run: pnpm nx affected --target=build --parallel --max-parallel=3 32 | - name: Test Projects 33 | run: pnpm nx affected --target=test --parallel --max-parallel=2 34 | - run: pnpm nx-cloud stop-all-agents 35 | - name: Tag main branch if all jobs succeed 36 | uses: nrwl/nx-tag-successful-ci-run@v1 37 | pr: 38 | runs-on: ubuntu-latest 39 | if: ${{ github.event_name == 'pull_request' }} 40 | steps: 41 | - uses: actions/checkout@v2 42 | with: 43 | ref: ${{ github.event.pull_request.head.ref }} 44 | fetch-depth: 0 45 | - name: Derive appropriate SHAs for base and head for `nx affected` commands 46 | uses: nrwl/nx-set-shas@v1 47 | - uses: actions/setup-node@v1 48 | with: 49 | node-version: 18.x 50 | - name: Install pnpm 51 | run: npm i -g pnpm 52 | - name: Install deps 53 | run: pnpm i --frozen-lockfile=false 54 | - name: Build Projects 55 | run: pnpm nx affected --target=build --parallel --max-parallel=3 56 | - name: Test Projects 57 | run: pnpm nx affected --target=test --parallel --max-parallel=2 58 | - run: pnpm nx-cloud stop-all-agents 59 | agents: 60 | runs-on: ubuntu-latest 61 | name: Agent 1 62 | timeout-minutes: 60 63 | strategy: 64 | matrix: 65 | agent: [ 1, 2, 3 ] 66 | steps: 67 | - uses: actions/checkout@v2 68 | - uses: actions/setup-node@v1 69 | with: 70 | node-version: 18.x 71 | - name: Install pnpm 72 | run: npm i -g pnpm 73 | - name: Install deps 74 | run: pnpm i --frozen-lockfile=false 75 | - name: Start Nx Agent ${{ matrix.agent }} 76 | run: pnpm nx-cloud start-agent 77 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | env: 9 | NX_CLOUD_DISTRIBUTED_EXECUTION: true 10 | NX_CLOUD_AUTH_TOKEN: ${{ secrets.NX_CLOUD_TOKEN }} 11 | 12 | jobs: 13 | release: 14 | name: Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout Repo 18 | uses: actions/checkout@master 19 | with: 20 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 21 | fetch-depth: 0 22 | 23 | - name: Setup Node.js 18.x 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: 18.x 27 | 28 | - name: Install pnpm 29 | run: npm i -g pnpm 30 | 31 | - name: Install Dependencies 32 | run: pnpm i --frozen-lockfile=false 33 | 34 | - name: Build Projects 35 | run: pnpm nx run-many --target=build --all 36 | 37 | - name: Update Workspace file 38 | run: sed -e "s|'packages\/|'dist/|" pnpm-workspace.yaml > pnpm-new.yaml && mv pnpm-new.yaml pnpm-workspace.yaml 39 | 40 | - name: Create Release Pull Request or Publish to npm 41 | id: changesets 42 | uses: changesets/action@v1 43 | with: 44 | publish: pnpm release 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 48 | - run: pnpm nx-cloud stop-all-agents 49 | 50 | agents: 51 | runs-on: ubuntu-latest 52 | name: Agent 1 53 | timeout-minutes: 60 54 | strategy: 55 | matrix: 56 | agent: [ 1, 2, 3 ] 57 | steps: 58 | - uses: actions/checkout@v2 59 | - uses: actions/setup-node@v1 60 | with: 61 | node-version: 18.x 62 | - name: Install pnpm 63 | run: npm i -g pnpm 64 | - name: Install deps 65 | run: pnpm i --frozen-lockfile=false 66 | - name: Start Nx Agent ${{ matrix.agent }} 67 | run: pnpm nx-cloud start-agent 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | **/node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | tags 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # System Files 39 | .DS_Store 40 | Thumbs.db 41 | 42 | .env 43 | 44 | .nx/cache 45 | .nx/workspace-data -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | 6 | /.nx/cache 7 | /.nx/workspace-data -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "proseWrap": "always" 5 | } 6 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "jsc": { 4 | "parser": { 5 | "syntax": "typescript", 6 | "decorators": true 7 | }, 8 | "transform": { 9 | "legacyDecorator": true, 10 | "decoratorMetadata": true 11 | }, 12 | "target": "es2017", 13 | "keepClassNames": true, 14 | "baseUrl": "." 15 | }, 16 | "module": { 17 | "type": "commonjs", 18 | "strictMode": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "firsttris.vscode-jest-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestLab 2 | 3 | This repo is to manage open source libraries that integrate with the NestJS framework, but don't quite belong in the core repo. Kind of like an experimental hold for packages to see how they do. I'll add more docs later when they start to matter more 4 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: [ 3 | '/packages/or-guard', 4 | '/packages/fastify-multer', 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset'); 2 | 3 | module.exports = { 4 | ...nxPreset, 5 | /* TODO: Update to latest Jest snapshotFormat 6 | * By default Nx has kept the older style of Jest Snapshot formats 7 | * to prevent breaking of any existing tests with snapshots. 8 | * It's recommend you update to the latest format. 9 | * You can do this by removing snapshotFormat property 10 | * and running tests with --update-snapshot flag. 11 | * Example: "nx affected --targets=test --update-snapshot" 12 | * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format 13 | */ 14 | snapshotFormat: { escapeString: true, printBasicPrototype: true }, 15 | }; 16 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasksRunnerOptions": { 3 | "default": { 4 | "options": { 5 | "canTrackAnalytics": false, 6 | "showUsageWarnings": true 7 | } 8 | } 9 | }, 10 | "targetDependencies": { 11 | "build": [ 12 | { 13 | "target": "build", 14 | "projects": "dependencies" 15 | } 16 | ] 17 | }, 18 | "workspaceLayout": { 19 | "appsDir": "apps", 20 | "libsDir": "packages" 21 | }, 22 | "namedInputs": { 23 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 24 | "sharedGlobals": [], 25 | "production": [ 26 | "default", 27 | "!{projectRoot}/.eslintrc.json", 28 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 29 | "!{projectRoot}/tsconfig.spec.json", 30 | "!{projectRoot}/jest.config.[jt]s", 31 | "!{projectRoot}/src/test-setup.[jt]s" 32 | ] 33 | }, 34 | "targetDefaults": { 35 | "build": { 36 | "inputs": ["production", "^production"], 37 | "cache": true 38 | }, 39 | "test": { 40 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 41 | "cache": true 42 | }, 43 | "@nx/eslint:lint": { 44 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"], 45 | "cache": true 46 | }, 47 | "@nx/jest:jest": { 48 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 49 | "cache": true, 50 | "options": { 51 | "passWithNoTests": true 52 | }, 53 | "configurations": { 54 | "ci": { 55 | "ci": true, 56 | "codeCoverage": true 57 | } 58 | } 59 | }, 60 | "@nx/vite:test": { 61 | "cache": true, 62 | "inputs": ["default", "^production"] 63 | }, 64 | "@nx/js:tsc": { 65 | "cache": true, 66 | "dependsOn": ["^build"], 67 | "inputs": ["production", "^production"] 68 | } 69 | }, 70 | "nxCloudAccessToken": "", 71 | "useInferencePlugins": false, 72 | "defaultBase": "main" 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nest-lab", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "nx serve", 7 | "build": "nx build", 8 | "test": "nx test", 9 | "lint": "nx workspace-lint && nx lint", 10 | "e2e": "nx e2e", 11 | "affected:apps": "nx affected:apps", 12 | "affected:libs": "nx affected:libs", 13 | "affected:build": "nx affected:build", 14 | "affected:e2e": "nx affected:e2e", 15 | "affected:test": "nx affected:test", 16 | "affected:lint": "nx affected:lint", 17 | "affected:dep-graph": "nx affected:dep-graph", 18 | "affected": "nx affected", 19 | "format": "nx format:write", 20 | "format:write": "nx format:write", 21 | "format:check": "nx format:check", 22 | "update": "nx migrate latest", 23 | "workspace-generator": "nx workspace-generator", 24 | "dep-graph": "nx dep-graph", 25 | "help": "nx help", 26 | "release": "pnpm changeset publish" 27 | }, 28 | "private": true, 29 | "devDependencies": { 30 | "@changesets/cli": "^2.23.2", 31 | "@nestjs/common": "11.0.5", 32 | "@nestjs/core": "11.0.5", 33 | "@nestjs/platform-express": "11.0.5", 34 | "@nestjs/platform-fastify": "11.0.5", 35 | "@nestjs/schematics": "11.0.0", 36 | "@nestjs/testing": "11.0.5", 37 | "@nestjs/throttler": "^6.4.0", 38 | "@nrwl/tao": "19.6.1", 39 | "@nx/eslint": "19.6.1", 40 | "@nx/eslint-plugin": "19.6.1", 41 | "@nx/jest": "19.6.1", 42 | "@nx/js": "19.6.1", 43 | "@nx/nest": "19.6.1", 44 | "@nx/vite": "^19.6.1", 45 | "@nx/web": "19.6.1", 46 | "@nx/workspace": "19.6.1", 47 | "@sinclair/typebox": "^0.31.14", 48 | "@swc-node/register": "~1.9.1", 49 | "@swc/core": "1.5.7", 50 | "@swc/helpers": "~0.5.11", 51 | "@swc/register": "^0.1.10", 52 | "@types/jest": "29.5.12", 53 | "@types/node": "18.19.9", 54 | "@types/supertest": "^2.0.12", 55 | "@typeschema/arktype": "^0.13.2", 56 | "@typeschema/core": "^0.13.2", 57 | "@typeschema/io-ts": "^0.13.3", 58 | "@typeschema/joi": "^0.13.3", 59 | "@typeschema/json": "^0.13.3", 60 | "@typeschema/main": "^0.13.10", 61 | "@typeschema/ow": "^0.13.3", 62 | "@typeschema/runtypes": "^0.13.2", 63 | "@typeschema/superstruct": "^0.13.2", 64 | "@typeschema/typebox": "^0.13.4", 65 | "@typeschema/valibot": "^0.13.5", 66 | "@typeschema/yup": "^0.13.3", 67 | "@typeschema/zod": "^0.13.3", 68 | "@typescript-eslint/eslint-plugin": "7.18.0", 69 | "@typescript-eslint/parser": "7.18.0", 70 | "@vitest/coverage-v8": "^1.0.4", 71 | "@vitest/ui": "^1.3.1", 72 | "ajv": "^8.17.1", 73 | "arktype": "1.0.21-alpha", 74 | "dotenv": "8.2.0", 75 | "eslint": "8.57.0", 76 | "eslint-config-prettier": "9.1.0", 77 | "fastify": "^5.2.1", 78 | "fastify-multer": "^2.0.3", 79 | "io-ts": "^2.2.20", 80 | "ioredis": "^5.4.1", 81 | "jest": "29.7.0", 82 | "jest-environment-jsdom": "29.7.0", 83 | "jest-environment-node": "^29.7.0", 84 | "joi": "^17.13.3", 85 | "nx": "19.6.1", 86 | "ow": "^0.28.2", 87 | "pactum": "^3.1.13", 88 | "prettier": "^2.7.1", 89 | "reflect-metadata": "^0.2.2", 90 | "runtypes": "^6.7.0", 91 | "rxjs": "^7.8.0", 92 | "superstruct": "^1.0.4", 93 | "supertest": "^6.2.4", 94 | "ts-jest": "29.1.0", 95 | "ts-node": "10.9.1", 96 | "tslib": "^2.4.0", 97 | "typescript": "5.5.4", 98 | "unplugin-swc": "^1.5.1", 99 | "valibot": "^0.31.1", 100 | "vite": "^5.4.2", 101 | "vitest": "^2.0.5", 102 | "yup": "^1.4.0", 103 | "zod": "^3.23.8" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmcdo29/nest-lab/8aa8b5d4cb1c7e79a4f6179abea37753912ca330/packages/.gitkeep -------------------------------------------------------------------------------- /packages/fastify-multer/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/fastify-multer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @nest-lab/fastify-multer 2 | 3 | ## 1.3.0 4 | 5 | ### Minor Changes 6 | 7 | - 45c426c: Now supporting NestJS v11 8 | 9 | ## 1.2.0 10 | 11 | ### Minor Changes 12 | 13 | - 2095ccf: Update peer deps to support Nest v10. No code changes 14 | 15 | ## 1.1.0 16 | 17 | ### Minor Changes 18 | 19 | - d72a963: export multer interface and use options from fastify-multer 20 | - 0b750cd: add file interface from fastify multer 21 | 22 | ## 1.0.2 23 | 24 | ### Patch Changes 25 | 26 | - 19eb008: Options now persist via being set in the module 27 | 28 | ## 1.0.1 29 | 30 | ### Patch Changes 31 | 32 | - 0ab9b86: allow for multiple `registerAsync` calls 33 | 34 | By moving the registration of the multipart content parse to a separate core 35 | module, the core module only gets activated once which allows for multiple 36 | `registerAsync` calls without calling the `fastify.register()` multiple times. 37 | This should resovle the error in #11. 38 | 39 | ## 1.0.0 40 | 41 | ### Major Changes 42 | 43 | - abacf8f: A File Upload package for NestJS when using fastify 44 | -------------------------------------------------------------------------------- /packages/fastify-multer/README.md: -------------------------------------------------------------------------------- 1 | # @nest-lab/fastify-multer 2 | 3 | Support for File Uploads when using the [`@nestjs/platform-fastify`](https://docs.nestjs.com/techniques/performance) adapter, using the [`fastify-multer`](https://www.npmjs.com/package/fastify-multer) library. 4 | 5 | ## Installation 6 | 7 | Simple install, 8 | 9 | ```shell 10 | npm i @nest-lab/fastify-multer 11 | yarn add @nest-lab/fastify-multer 12 | pnpm i @nest-lab/fastify-multer 13 | ``` 14 | 15 | ## Usage 16 | 17 | Use this exactly like you would the [`MulterModule`](https://docs.nestjs.com/techniques/file-upload) from `@nestjs/platform-express`. The only difference is there is also a `NoFilesInterceptor` for when you jsut want to parse `multipart/form-data` and you __must__ have `FastifyMulterModule` imported _somewhere_ so that the `multipart/form-data` content parser gets registered with fastify. 18 | -------------------------------------------------------------------------------- /packages/fastify-multer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nest-lab/fastify-multer", 3 | "version": "1.3.0", 4 | "description": "A File Upload package for NestJS when using fastify", 5 | "keywords": [ 6 | "fastify", 7 | "nestjs", 8 | "file upload" 9 | ], 10 | "license": "MIT", 11 | "author": { 12 | "name": "Jay McDoniel", 13 | "email": "me@jaymcdoniel.dev" 14 | }, 15 | "publishConfig": { 16 | "access": "public" 17 | }, 18 | "repository": { 19 | "type": "github", 20 | "url": "https://github.com/jmcdo29/nest-lab", 21 | "directory": "packages/fastify-multer" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/jmcdo29/nest-lab/issues" 25 | }, 26 | "type": "commonjs", 27 | "dependencies": { 28 | "fastify-multer": "2.0.3" 29 | }, 30 | "peerDependencies": { 31 | "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", 32 | "@nestjs/platform-fastify": "^9.0.0 || ^10.0.0 || ^11.0.0", 33 | "rxjs": "^7.0.0" 34 | }, 35 | "peerDependenciesMeta": { 36 | "@nestjs/common": { 37 | "optional": false 38 | }, 39 | "@nestjs/platform-fastify": { 40 | "optional": false 41 | }, 42 | "rxjs": { 43 | "optional": false 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/fastify-multer/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-multer", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/fastify-multer/src", 5 | "projectType": "library", 6 | "targets": { 7 | "build": { 8 | "executor": "@nrwl/js:tsc", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/packages/fastify-multer", 12 | "tsConfig": "packages/fastify-multer/tsconfig.lib.json", 13 | "packageJson": "packages/fastify-multer/package.json", 14 | "main": "packages/fastify-multer/src/index.ts", 15 | "assets": ["packages/fastify-multer/*.md"], 16 | "updateBuildableProjectDepsInPackageJson": true, 17 | "buildableProjectDepsInPackageJsonType": "dependencies" 18 | } 19 | }, 20 | "publish": { 21 | "executor": "nx:run-commands", 22 | "options": { 23 | "cwd": "dist/packages/fastify-multer", 24 | "command": "pnpm publish" 25 | }, 26 | "dependsOn": [ 27 | { 28 | "target": "build" 29 | } 30 | ] 31 | }, 32 | "lint": { 33 | "executor": "@nx/eslint:lint", 34 | "outputs": ["{options.outputFile}"] 35 | }, 36 | "test": { 37 | "executor": "nx:run-commands", 38 | "options": { 39 | "command": "node -r @swc/register --test packages/fastify-multer/test/index.spec.ts" 40 | }, 41 | "configurations": { 42 | "local": { 43 | "command": "node -r @swc/register packages/fastify-multer/test/index.spec.ts" 44 | } 45 | } 46 | } 47 | }, 48 | "tags": [] 49 | } 50 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/fastify-multer.module'; 2 | export * from './lib/interceptors'; 3 | export * from './lib/interfaces'; 4 | 5 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/fastify-multer-core.module.ts: -------------------------------------------------------------------------------- 1 | import { Module, OnApplicationBootstrap } from '@nestjs/common'; 2 | import { HttpAdapterHost } from '@nestjs/core'; 3 | import { FastifyAdapter } from '@nestjs/platform-fastify'; 4 | import multer from 'fastify-multer'; 5 | 6 | @Module({}) 7 | export class FastifyCoreModule implements OnApplicationBootstrap { 8 | constructor( 9 | private readonly httpAdapterHost: HttpAdapterHost 10 | ) {} 11 | onApplicationBootstrap() { 12 | const fastify = this.httpAdapterHost.httpAdapter.getInstance(); 13 | if (!fastify.hasContentTypeParser('multipart')) { 14 | fastify.register(multer.contentParser); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/fastify-multer.module-definition.ts: -------------------------------------------------------------------------------- 1 | import { ConfigurableModuleBuilder } from '@nestjs/common'; 2 | import { MulterModuleOptions } from './interfaces'; 3 | 4 | export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = 5 | new ConfigurableModuleBuilder().build(); 6 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/fastify-multer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FastifyCoreModule } from './fastify-multer-core.module'; 3 | import { 4 | ConfigurableModuleClass, 5 | MODULE_OPTIONS_TOKEN, 6 | } from './fastify-multer.module-definition'; 7 | import { MULTER_OPTIONS } from './files.constants'; 8 | import { MulterModuleOptions } from './interfaces'; 9 | 10 | @Module({ 11 | imports: [FastifyCoreModule], 12 | controllers: [], 13 | providers: [ 14 | { 15 | provide: MULTER_OPTIONS, 16 | useFactory: (options?: MulterModuleOptions) => ({ ...(options ?? {}) }), 17 | inject: [{ token: MODULE_OPTIONS_TOKEN, optional: true }], 18 | }, 19 | ], 20 | exports: [MULTER_OPTIONS], 21 | }) 22 | export class FastifyMulterModule extends ConfigurableModuleClass {} 23 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/files.constants.ts: -------------------------------------------------------------------------------- 1 | import { MODULE_OPTIONS_TOKEN } from './fastify-multer.module-definition'; 2 | 3 | export const MULTER_OPTIONS = 'MULTER_OPTIONS'; 4 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/interceptors/any-files.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Inject, 5 | mixin, 6 | NestInterceptor, 7 | Optional, 8 | Type, 9 | } from '@nestjs/common'; 10 | import multer from 'fastify-multer'; 11 | import { Observable } from 'rxjs'; 12 | import { MULTER_OPTIONS } from '../files.constants'; 13 | import { MulterModuleOptions } from '../interfaces'; 14 | import { MulterOptions } from '../interfaces/multer-options.interface'; 15 | import { transformException } from '../multer/multer.utils'; 16 | 17 | type MulterInstance = ReturnType; 18 | 19 | export function AnyFilesInterceptor( 20 | localOptions?: MulterOptions 21 | ): Type { 22 | class MixinInterceptor implements NestInterceptor { 23 | protected multer: MulterInstance; 24 | 25 | constructor( 26 | @Optional() 27 | @Inject(MULTER_OPTIONS) 28 | options: MulterModuleOptions = {} 29 | ) { 30 | this.multer = multer({ 31 | ...options, 32 | ...localOptions, 33 | }); 34 | } 35 | 36 | async intercept( 37 | context: ExecutionContext, 38 | next: CallHandler 39 | ): Promise> { 40 | const ctx = context.switchToHttp(); 41 | 42 | await new Promise((resolve, reject) => 43 | // @ts-expect-errornot using method as pre-handler, so signature is different 44 | this.multer.any()(ctx.getRequest(), ctx.getResponse(), (err: Error) => { 45 | if (err) { 46 | const error = transformException(err); 47 | return reject(error); 48 | } 49 | resolve(); 50 | }) 51 | ); 52 | return next.handle(); 53 | } 54 | } 55 | const Interceptor = mixin(MixinInterceptor); 56 | return Interceptor; 57 | } 58 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/interceptors/file-fields.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Inject, 5 | mixin, 6 | NestInterceptor, 7 | Optional, 8 | Type, 9 | } from '@nestjs/common'; 10 | import multer from 'fastify-multer'; 11 | import { Observable } from 'rxjs'; 12 | import { MULTER_OPTIONS } from '../files.constants'; 13 | import { MulterModuleOptions } from '../interfaces'; 14 | import { 15 | MulterField, 16 | MulterOptions, 17 | } from '../interfaces/multer-options.interface'; 18 | import { transformException } from '../multer/multer.utils'; 19 | 20 | type MulterInstance = ReturnType; 21 | 22 | export function FileFieldsInterceptor( 23 | uploadFields: MulterField[], 24 | localOptions?: MulterOptions 25 | ): Type { 26 | class MixinInterceptor implements NestInterceptor { 27 | protected multer: MulterInstance; 28 | 29 | constructor( 30 | @Optional() 31 | @Inject(MULTER_OPTIONS) 32 | options: MulterModuleOptions = {} 33 | ) { 34 | this.multer = multer({ 35 | ...options, 36 | ...localOptions, 37 | }); 38 | } 39 | 40 | async intercept( 41 | context: ExecutionContext, 42 | next: CallHandler 43 | ): Promise> { 44 | const ctx = context.switchToHttp(); 45 | 46 | await new Promise((resolve, reject) => 47 | // @ts-expect-errornot using method as pre-handler, so signature is different 48 | this.multer.fields(uploadFields)( 49 | ctx.getRequest(), 50 | ctx.getResponse(), 51 | (err: Error) => { 52 | if (err) { 53 | const error = transformException(err); 54 | return reject(error); 55 | } 56 | resolve(); 57 | } 58 | ) 59 | ); 60 | return next.handle(); 61 | } 62 | } 63 | const Interceptor = mixin(MixinInterceptor); 64 | return Interceptor; 65 | } 66 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/interceptors/file.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Inject, 5 | mixin, 6 | NestInterceptor, 7 | Optional, 8 | Type, 9 | } from '@nestjs/common'; 10 | import multer from 'fastify-multer'; 11 | import { Observable } from 'rxjs'; 12 | import { MULTER_OPTIONS } from '../files.constants'; 13 | import { MulterModuleOptions } from '../interfaces'; 14 | import { MulterOptions } from '../interfaces/multer-options.interface'; 15 | import { transformException } from '../multer/multer.utils'; 16 | 17 | type MulterInstance = ReturnType; 18 | 19 | export function FileInterceptor( 20 | fieldName: string, 21 | localOptions?: MulterOptions 22 | ): Type { 23 | class MixinInterceptor implements NestInterceptor { 24 | protected multer: MulterInstance; 25 | 26 | constructor( 27 | @Optional() 28 | @Inject(MULTER_OPTIONS) 29 | options: MulterModuleOptions = {}, 30 | ) { 31 | this.multer = multer({ 32 | ...options, 33 | ...localOptions, 34 | }); 35 | } 36 | 37 | async intercept( 38 | context: ExecutionContext, 39 | next: CallHandler 40 | ): Promise> { 41 | const ctx = context.switchToHttp(); 42 | await new Promise((resolve, reject) => 43 | // @ts-expect-errornot using method as pre-handler, so signature is different 44 | this.multer.single(fieldName)( 45 | ctx.getRequest(), 46 | ctx.getResponse(), 47 | (err: Error) => { 48 | if (err) { 49 | const error = transformException(err); 50 | return reject(error); 51 | } 52 | resolve(); 53 | } 54 | ) 55 | ); 56 | return next.handle(); 57 | } 58 | } 59 | const Interceptor = mixin(MixinInterceptor); 60 | return Interceptor; 61 | } 62 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/interceptors/files.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Inject, 5 | mixin, 6 | NestInterceptor, 7 | Optional, 8 | Type, 9 | } from '@nestjs/common'; 10 | import multer from 'fastify-multer'; 11 | import { Observable } from 'rxjs'; 12 | import { MULTER_OPTIONS } from '../files.constants'; 13 | import { MulterModuleOptions } from '../interfaces'; 14 | import { MulterOptions } from '../interfaces/multer-options.interface'; 15 | import { transformException } from '../multer/multer.utils'; 16 | 17 | type MulterInstance = ReturnType; 18 | 19 | export function FilesInterceptor( 20 | fieldName: string, 21 | maxCount?: number, 22 | localOptions?: MulterOptions 23 | ): Type { 24 | class MixinInterceptor implements NestInterceptor { 25 | protected multer: MulterInstance; 26 | 27 | constructor( 28 | @Optional() 29 | @Inject(MULTER_OPTIONS) 30 | options: MulterModuleOptions = {} 31 | ) { 32 | this.multer = multer({ 33 | ...options, 34 | ...localOptions, 35 | }); 36 | } 37 | 38 | async intercept( 39 | context: ExecutionContext, 40 | next: CallHandler 41 | ): Promise> { 42 | const ctx = context.switchToHttp(); 43 | 44 | await new Promise((resolve, reject) => 45 | // @ts-expect-errornot using method as pre-handler, so signature is different 46 | this.multer.array(fieldName, maxCount)( 47 | ctx.getRequest(), 48 | ctx.getResponse(), 49 | (err: Error) => { 50 | if (err) { 51 | const error = transformException(err); 52 | return reject(error); 53 | } 54 | resolve(); 55 | } 56 | ) 57 | ); 58 | return next.handle(); 59 | } 60 | } 61 | const Interceptor = mixin(MixinInterceptor); 62 | return Interceptor; 63 | } 64 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/interceptors/index.ts: -------------------------------------------------------------------------------- 1 | export * from './any-files.interceptor'; 2 | export * from './file-fields.interceptor'; 3 | export * from './file.interceptor'; 4 | export * from './files.interceptor'; 5 | export * from './no-files.interceptor'; 6 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/interceptors/no-files.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallHandler, 3 | ExecutionContext, 4 | Inject, 5 | mixin, 6 | NestInterceptor, 7 | Optional, 8 | Type, 9 | } from '@nestjs/common'; 10 | import multer from 'fastify-multer'; 11 | import { Observable } from 'rxjs'; 12 | import { MULTER_OPTIONS } from '../files.constants'; 13 | import { MulterModuleOptions } from '../interfaces'; 14 | import { transformException } from '../multer/multer.utils'; 15 | 16 | type MulterInstance = ReturnType; 17 | 18 | export function NoFilesInterceptor(): Type { 19 | class MixinInterceptor implements NestInterceptor { 20 | protected multer: MulterInstance; 21 | 22 | constructor( 23 | @Optional() 24 | @Inject(MULTER_OPTIONS) 25 | options: MulterModuleOptions = {} 26 | ) { 27 | this.multer = multer({ 28 | ...options, 29 | }); 30 | } 31 | 32 | async intercept( 33 | context: ExecutionContext, 34 | next: CallHandler 35 | ): Promise> { 36 | const ctx = context.switchToHttp(); 37 | 38 | await new Promise((resolve, reject) => 39 | // @ts-expect-errornot using method as pre-handler, so signature is different 40 | this.multer.none()( 41 | ctx.getRequest(), 42 | ctx.getResponse(), 43 | (err: Error) => { 44 | if (err) { 45 | const error = transformException(err); 46 | return reject(error); 47 | } 48 | resolve(); 49 | } 50 | ) 51 | ); 52 | return next.handle(); 53 | } 54 | } 55 | const Interceptor = mixin(MixinInterceptor); 56 | return Interceptor; 57 | } 58 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/interfaces/file-uploads.interface.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { ModuleMetadata, Provider } from '@nestjs/common/interfaces'; 3 | import { MulterOptions } from './multer-options.interface'; 4 | 5 | export type MulterModuleOptions = MulterOptions; 6 | 7 | export interface MulterOptionsFactory { 8 | createMulterOptions(): Promise | MulterModuleOptions; 9 | } 10 | 11 | export interface MulterModuleAsyncOptions 12 | extends Pick { 13 | useExisting?: Type; 14 | useClass?: Type; 15 | useFactory?: ( 16 | ...args: unknown[] 17 | ) => Promise | MulterModuleOptions; 18 | inject?: Provider[]; 19 | } 20 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './multer-options.interface'; 2 | export * from './file-uploads.interface'; 3 | export * from "fastify-multer/lib/interfaces"; 4 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/interfaces/multer-options.interface.ts: -------------------------------------------------------------------------------- 1 | import {Options} from "fastify-multer/lib/interfaces"; 2 | 3 | 4 | /** 5 | * @see https://github.com/expressjs/multer 6 | */ 7 | export interface MulterOptions extends Options{} 8 | 9 | 10 | export interface MulterField { 11 | /** The field name. */ 12 | name: string; 13 | /** Optional maximum number of files per field to accept. */ 14 | maxCount?: number; 15 | } 16 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/multer/multer.constants.ts: -------------------------------------------------------------------------------- 1 | export const multerExceptions = { 2 | LIMIT_PART_COUNT: 'Too many parts', 3 | LIMIT_FILE_SIZE: 'File too large', 4 | LIMIT_FILE_COUNT: 'Too many files', 5 | LIMIT_FIELD_KEY: 'Field name too long', 6 | LIMIT_FIELD_VALUE: 'Field value too long', 7 | LIMIT_FIELD_COUNT: 'Too many fields', 8 | LIMIT_UNEXPECTED_FILE: 'Unexpected field', 9 | }; 10 | -------------------------------------------------------------------------------- /packages/fastify-multer/src/lib/multer/multer.utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | HttpException, 4 | PayloadTooLargeException, 5 | } from '@nestjs/common'; 6 | import { multerExceptions } from './multer.constants'; 7 | 8 | export function transformException(error: Error | undefined) { 9 | if (!error || error instanceof HttpException) { 10 | return error; 11 | } 12 | switch (error.message) { 13 | case multerExceptions.LIMIT_FILE_SIZE: 14 | return new PayloadTooLargeException(error.message); 15 | case multerExceptions.LIMIT_FILE_COUNT: 16 | case multerExceptions.LIMIT_FIELD_KEY: 17 | case multerExceptions.LIMIT_FIELD_VALUE: 18 | case multerExceptions.LIMIT_FIELD_COUNT: 19 | case multerExceptions.LIMIT_UNEXPECTED_FILE: 20 | case multerExceptions.LIMIT_PART_COUNT: 21 | return new BadRequestException(error.message); 22 | } 23 | return error; 24 | } 25 | -------------------------------------------------------------------------------- /packages/fastify-multer/test/file-upload/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Post, 5 | UploadedFile, 6 | UploadedFiles, 7 | UseInterceptors, 8 | } from '@nestjs/common'; 9 | import { memoryStorage } from 'fastify-multer/lib'; 10 | import { 11 | AnyFilesInterceptor, 12 | FileFieldsInterceptor, 13 | FileInterceptor, 14 | FilesInterceptor, 15 | NoFilesInterceptor, 16 | } from '../../../src'; 17 | 18 | @Controller() 19 | export class AppController { 20 | @Post('single') 21 | @UseInterceptors(FileInterceptor('file', { storage: memoryStorage() })) 22 | uploadSingleFile(@UploadedFile() file: unknown) { 23 | return { success: !!file }; 24 | } 25 | 26 | @Post('multiple') 27 | @UseInterceptors(FilesInterceptor('file', 10, { storage: memoryStorage() })) 28 | uploadMultipleFiles(@UploadedFiles() files: unknown[]) { 29 | return { success: !!files.length, fileCount: files.length }; 30 | } 31 | 32 | @Post('any') 33 | @UseInterceptors(AnyFilesInterceptor({ storage: memoryStorage() })) 34 | uploadAnyFiles(@UploadedFiles() files: unknown[]) { 35 | return { success: !!files.length, fileCount: files.length }; 36 | } 37 | 38 | @Post('fields') 39 | @UseInterceptors( 40 | FileFieldsInterceptor([{ name: 'profile' }, { name: 'avatar' }], { 41 | storage: memoryStorage(), 42 | }) 43 | ) 44 | uploadFileFieldsFiles( 45 | @UploadedFiles() files: { profile?: unknown[]; avatar?: unknown[] } 46 | ) { 47 | return { 48 | success: !!((files.profile?.length ?? 0) + (files.avatar?.length ?? 0)), 49 | fileCount: (files.profile?.length ?? 0) + (files.avatar?.length ?? 0), 50 | }; 51 | } 52 | 53 | @Post('none') 54 | @UseInterceptors(NoFilesInterceptor()) 55 | noFilesAllowed( 56 | @Body() body: Record, 57 | @UploadedFiles() files: unknown[], 58 | @UploadedFile() file: unknown 59 | ) { 60 | return { success: !files && !file && !!body }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/fastify-multer/test/file-upload/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { FastifyMulterModule } from '../../../src'; 3 | import { AppController } from './app.controller'; 4 | 5 | @Module({ 6 | imports: [FastifyMulterModule], 7 | controllers: [AppController] 8 | }) 9 | export class AppModule {} 10 | -------------------------------------------------------------------------------- /packages/fastify-multer/test/file-upload/file-upload.spec.ts: -------------------------------------------------------------------------------- 1 | import { FastifyAdapter } from '@nestjs/platform-fastify'; 2 | import { Test } from '@nestjs/testing'; 3 | import { join } from 'node:path'; 4 | import { test } from 'node:test'; 5 | import { request, spec } from 'pactum'; 6 | import { AppModule } from './app/app.module'; 7 | 8 | export const uploadTests = test('Fastify File Upload', async (t) => { 9 | const modRef = await Test.createTestingModule({ 10 | imports: [AppModule], 11 | }).compile(); 12 | const app = modRef.createNestApplication(new FastifyAdapter()); 13 | await app.listen(0); 14 | const url = await app.getUrl(); 15 | request.setBaseUrl(url.replace('[::1]', 'localhost')); 16 | await t.test('Single File Upload', async () => { 17 | await spec() 18 | .post('/single') 19 | .withFile('file', join(process.cwd(), 'package.json')) 20 | .expectStatus(201) 21 | .expectBody({ success: true }) 22 | .toss(); 23 | }); 24 | await t.test('Multiple File Uploads', async () => { 25 | await spec() 26 | .post('/multiple') 27 | .withFile('file', join(process.cwd(), 'package.json')) 28 | .withFile('file', join(process.cwd(), '.eslintrc.json')) 29 | .withMultiPartFormData('nonFile', 'Hello World!') 30 | .expectStatus(201) 31 | .expectBody({ success: true, fileCount: 2 }) 32 | .toss(); 33 | }); 34 | await t.test('Any File Upload', async () => { 35 | await spec() 36 | .post('/any') 37 | .withFile('fil', join(process.cwd(), 'package.json')) 38 | .withMultiPartFormData('field', 'value') 39 | .expectStatus(201) 40 | .expectBody({ success: true, fileCount: 1 }) 41 | .toss(); 42 | }); 43 | await t.test('File Fields Upload - profile field', async () => { 44 | await spec() 45 | .post('/fields') 46 | .withFile('profile', join(process.cwd(), 'package.json')) 47 | .expectStatus(201) 48 | .expectBody({ success: true, fileCount: 1 }) 49 | .toss(); 50 | }); 51 | await t.test('File Fields Upload - avatar field', async () => { 52 | await spec() 53 | .post('/fields') 54 | .withFile('avatar', join(process.cwd(), 'package.json')) 55 | .expectStatus(201) 56 | .expectBody({ success: true, fileCount: 1 }) 57 | .toss(); 58 | }); 59 | await t.test('File Fields Upload - profile and avatar fields', async () => { 60 | await spec() 61 | .post('/fields') 62 | .withFile('profile', join(process.cwd(), 'package.json')) 63 | .withFile('avatar', join(process.cwd(), 'package.json')) 64 | .expectStatus(201) 65 | .expectBody({ success: true, fileCount: 2 }) 66 | .toss(); 67 | }); 68 | await t.test('No File Upload - 201, no file', async () => { 69 | await spec() 70 | .post('/none') 71 | .withMultiPartFormData('no', 'files') 72 | .expectStatus(201) 73 | .expectBody({ success: true }) 74 | .toss(); 75 | }); 76 | await t.test('No File Upload - 400, with file', async () => { 77 | await spec() 78 | .post('/none') 79 | .withFile('file', join(process.cwd(), 'package.json')) 80 | .expectStatus(400) 81 | .toss(); 82 | }); 83 | await app.close(); 84 | }); 85 | -------------------------------------------------------------------------------- /packages/fastify-multer/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { uploadTests } from './file-upload/file-upload.spec'; 2 | import { multipleImportsTest } from './multiple-imports/multiple-imports.spec'; 3 | import { uploadWithModuleOptionsTest } from './uploads-from-options/upload-from-options.spec'; 4 | 5 | (async () => { 6 | await Promise.all([ 7 | uploadTests, 8 | multipleImportsTest, 9 | uploadWithModuleOptionsTest, 10 | ]); 11 | })(); 12 | -------------------------------------------------------------------------------- /packages/fastify-multer/test/multiple-imports/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BarModule } from '../bar/bar.module'; 3 | import { FooModule } from '../foo/foo.module'; 4 | 5 | @Module({ 6 | imports: [BarModule, FooModule], 7 | }) 8 | export class AppModule {} 9 | -------------------------------------------------------------------------------- /packages/fastify-multer/test/multiple-imports/bar/bar.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { diskStorage } from 'fastify-multer/lib'; 3 | import { FastifyMulterModule } from '../../../src'; 4 | 5 | @Module({ 6 | imports: [ 7 | FastifyMulterModule.registerAsync({ 8 | useFactory: () => ({ 9 | storage: diskStorage({ destination: '/tmp/uploads/' }), 10 | }), 11 | }), 12 | ], 13 | }) 14 | export class BarModule {} 15 | -------------------------------------------------------------------------------- /packages/fastify-multer/test/multiple-imports/foo/foo.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { memoryStorage } from 'fastify-multer/lib'; 3 | import { FastifyMulterModule } from '../../../src'; 4 | 5 | @Module({ 6 | imports: [ 7 | FastifyMulterModule.registerAsync({ 8 | useFactory: () => ({ 9 | storage: memoryStorage(), 10 | }), 11 | }), 12 | ], 13 | }) 14 | export class FooModule {} 15 | -------------------------------------------------------------------------------- /packages/fastify-multer/test/multiple-imports/multiple-imports.spec.ts: -------------------------------------------------------------------------------- 1 | import assert = require('node:assert'); 2 | import { Test } from '@nestjs/testing'; 3 | import test from 'node:test'; 4 | import { AppModule } from './app/app.module'; 5 | import { FastifyAdapter } from '@nestjs/platform-fastify'; 6 | 7 | export const multipleImportsTest = test('Multiple Import Tests', async (t) => { 8 | await t.test('Should be true', async () => { 9 | const modRef = await Test.createTestingModule({ 10 | imports: [AppModule], 11 | }).compile(); 12 | const app = modRef.createNestApplication(new FastifyAdapter()); 13 | try { 14 | await app.listen(0); 15 | assert(app !== undefined); 16 | } catch { 17 | assert(false); 18 | } finally { 19 | await app.close(); 20 | } 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/fastify-multer/test/uploads-from-options/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Post, 4 | UploadedFile, 5 | UseInterceptors, 6 | } from '@nestjs/common'; 7 | import { FileInterceptor } from '../../../src'; 8 | 9 | @Controller() 10 | export class AppController { 11 | @Post() 12 | @UseInterceptors(FileInterceptor('file')) 13 | uploadFile(@UploadedFile() file: { filename: string }) { 14 | return { filename: file.filename }; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/fastify-multer/test/uploads-from-options/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { join } from 'node:path'; 3 | import { FastifyMulterModule } from '../../../src'; 4 | import { AppController } from './app.controller'; 5 | 6 | @Module({ 7 | imports: [ 8 | FastifyMulterModule.register({ 9 | dest: join(process.cwd(), 'uploads'), 10 | }), 11 | ], 12 | controllers: [AppController], 13 | }) 14 | export class AppModule {} 15 | -------------------------------------------------------------------------------- /packages/fastify-multer/test/uploads-from-options/upload-from-options.spec.ts: -------------------------------------------------------------------------------- 1 | import { FastifyAdapter } from '@nestjs/platform-fastify'; 2 | import { Test } from '@nestjs/testing'; 3 | import { unlink } from 'fs/promises'; 4 | import { join } from 'node:path'; 5 | import { test } from 'node:test'; 6 | import { request, spec } from 'pactum'; 7 | import { AppModule } from './app/app.module'; 8 | 9 | export const uploadWithModuleOptionsTest = 10 | test('Fastify File Upload with Module Options', async (t) => { 11 | const modRef = await Test.createTestingModule({ 12 | imports: [AppModule], 13 | }).compile(); 14 | const app = modRef.createNestApplication(new FastifyAdapter()); 15 | await app.listen(0); 16 | const url = await app.getUrl(); 17 | request.setBaseUrl(url.replace('[::1]', 'localhost')); 18 | let filePath = ''; 19 | await t.test('It should upload the file to the disk', async () => { 20 | await spec() 21 | .post('/') 22 | .withFile('file', join(process.cwd(), 'package.json')) 23 | .expectStatus(201) 24 | .returns(({ res }) => { 25 | filePath = (res.json as any).filename; 26 | }) 27 | .toss(); 28 | }); 29 | await unlink(join(process.cwd(), 'uploads', filePath)); 30 | await app.close(); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/fastify-multer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | }, 6 | "files": [], 7 | "include": [], 8 | "references": [ 9 | { 10 | "path": "./tsconfig.lib.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/fastify-multer/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": [], 7 | "target": "es2021" 8 | }, 9 | "include": ["**/*.ts"], 10 | "exclude": ["jest.config.ts", "**/*.spec.ts", "**/*.test.ts", "test"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/fastify-multer/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["jest.config.ts", "**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/or-guard/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [] 3 | } 4 | -------------------------------------------------------------------------------- /packages/or-guard/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": { 8 | } 9 | }, 10 | { 11 | "files": ["*.ts", "*.tsx"], 12 | "rules": {} 13 | }, 14 | { 15 | "files": ["*.js", "*.jsx"], 16 | "rules": {} 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/or-guard/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @nest-lab/or-guard 2 | 3 | ## 2.6.0 4 | 5 | ### Minor Changes 6 | 7 | - 45c426c: Now supporting NestJS v11 8 | 9 | ## 2.5.0 10 | 11 | ### Minor Changes 12 | 13 | - 458f98f: Allow for the `OrGuard` to handle throwing the last error or a custom 14 | error when it fails to pass one of the guards. 15 | 16 | ## 2.4.1 17 | 18 | ### Patch Changes 19 | 20 | - 7972281: Use `{strict: false}` when calling modRef.get() to ensure guards can 21 | come from other modules 22 | 23 | ## 2.4.0 24 | 25 | ### Minor Changes 26 | 27 | - 3a767b5: Allow for AndGuard guards to be ran in sequential order 28 | 29 | ## 2.3.0 30 | 31 | ### Minor Changes 32 | 33 | - bc9bbdf: Add a new AndGuard to handle complex logical cases 34 | 35 | With the new `AndGuard` it is now possible to create logical cases like 36 | `(A && B) || C` using the `OrGuard` and a composite guard approach. Check out 37 | the docs for more info 38 | 39 | ## 2.2.0 40 | 41 | ### Minor Changes 42 | 43 | - cb1b1ee: Update peer deps to support Nest v10. No code changes 44 | 45 | ## 2.1.0 46 | 47 | ### Minor Changes 48 | 49 | - 4f15fe7: Support Nest v9 50 | 51 | ## 2.0.0 52 | 53 | ### Major Changes 54 | 55 | - a098b52: Update Nest to v8 and RxJS to v7 56 | 57 | ## 1.0.0 58 | 59 | ### Major Changes 60 | 61 | - 3bb10ce: Publish the OrGuard 62 | -------------------------------------------------------------------------------- /packages/or-guard/README.md: -------------------------------------------------------------------------------- 1 | # @nest-lab/or-guard 2 | 3 | This library contains a two guards that allows for checking multiple guards and 4 | creating complex logical statements based on the results of those guards for if 5 | the request should be completed or not. 6 | 7 | ## Installation 8 | 9 | Pick your favorite package manager and install. Should be straight forward. 10 | 11 | ```sh 12 | npm i @nest-lab/or-guard 13 | yarn add @nest-lab/or-guard 14 | pnpm i @nest-lab/or-guard 15 | ``` 16 | 17 | ## OrGuard 18 | 19 | To use the `OrGuard`, there are a couple of things that need to happen, due to 20 | how the guard resolves the guards it's going to be using. 21 | 22 | First, make sure to add all the guards the `OrGuard` will be using to the 23 | current module's `providers` array. Enhancer in Nest are just specialized 24 | providers after all. This will allow the `OrGuard` to use a `ModuleRef` to get 25 | these guards. The guards can either be registered directly as providers, or set 26 | up as custom providers and you may use an injection token reference. Make sure, 27 | that if you use a custom provider, the _instance_ of the guard is what is tied 28 | to the token, not the reference to the class. 29 | 30 | Second, make sure **none** of these guards are `REQUEST` or `TRANSIENT` scoped, 31 | as this **will** make the `OrGuard` throw an error. 32 | 33 | Third, make use of it! The `OrGuard` takes in an array of guard to use for the 34 | first parameter, and an optional second parameter for options as described 35 | below. 36 | 37 | > **important**: for Nest v7, use `@nest-lab/or-guard@1.0.0`, for Nest v8, 38 | > please use v2 39 | 40 | ```ts 41 | OrGuard(guards: Array | InjectionToken>, orGuardOptions?: OrGuardOptions): CanActivate 42 | ``` 43 | 44 | - `guards`: an array of guards or injection tokens for the `OrGuard` to resolve 45 | and test 46 | - `orGuardOptions`: an optional object with properties to modify how the 47 | `OrGuard` functions 48 | 49 | ```ts 50 | interface OrGuardOptions { 51 | throwOnFirstError?: boolean; 52 | throwLastError?: boolean; 53 | throwError?: object | ((errors: unknown[]) => unknown); 54 | } 55 | ``` 56 | 57 | - `throwOnFirstError`: a boolean to tell the `OrGuard` whether to throw if an 58 | error is encountered or if the error should be considered a `return false`. 59 | The default value is `false`. If this is set to `true`, the **first** error 60 | encountered will lead to the same error being thrown. 61 | - `throwLastError`: a boolean to tell the `OrGuard` if the last error should be 62 | handled with `return false` or just thrown. The default value is `false`. If 63 | this is set to `true`, the **last** error encountered will lead to the same 64 | error being thrown. 65 | - `throwError`: provide a custom error to throw if all guards fail or provide a function 66 | to receive all encountered errors and return a custom error to throw. 67 | 68 | > **Note**: guards are ran in a non-deterministic order. All guard returns are 69 | > transformed into Observables and ran concurrently to ensure the fastest 70 | > response time possible. 71 | 72 | ## AndGuard 73 | 74 | Just like the `OrGuard`, you can create a logic grouping of situations that 75 | should pass. This is Nest's default when there are multiple guards passed to the 76 | `@UseGuards()` decorator; however, there are situations where it would be useful 77 | to use an `AndGuard` inside of an `OrGuard` to be able to create logic like 78 | `(A && B) || C`. With using an `AndGuard` inside of an `OrGuard`, you'll most 79 | likely want to create a dedicated [custom provider][customprov] for the guard 80 | like so: 81 | 82 | ```typescript 83 | { 84 | provide: AndGuardToken, 85 | useClass: AndGuard([GuardA, GuardB]) 86 | } 87 | ``` 88 | 89 | With this added to the module's providers where you plan to use the related 90 | `OrGuard` you can use the following in a controller or resolve: 91 | 92 | ```typescript 93 | @UseGuards(OrGuard([AndGuardToken, GuardC])) 94 | ``` 95 | 96 | And this library will set up the handling of the logic for 97 | `(GuardA && GuardB) || GuardC` without having to worry about the complexities 98 | under the hood. 99 | 100 | ```ts 101 | AndGuard(guards: Array | InjectionToken>, andGuardOptions?: AndGuardOptions): CanActivate 102 | ``` 103 | 104 | - `guards`: an array of guards or injection tokens for the `AndGuard` to resolve 105 | and test 106 | - `andGuardOptions`: an optional object with properties to modify how the 107 | `AndGuard` functions 108 | 109 | ```ts 110 | interface AndGuardOptions { 111 | // immediately stop all other guards and throw an error 112 | throwOnFirstError?: boolean; 113 | // run the guards in order they are declared in the array rather than in parallel 114 | sequential?: boolean; 115 | } 116 | ``` 117 | 118 | ## Local Development 119 | 120 | Feel free to pull down the repository and work locally. If any changes are made, 121 | please make sure tests are added to ensure the functionality works and nothing 122 | is broken. 123 | 124 | ### Running unit tests 125 | 126 | Run `nx test or-guard` to execute the unit tests via [Jest](https://jestjs.io). 127 | 128 | [customprov]: https://docs.nestjs.com/fundamentals/custom-providers 129 | -------------------------------------------------------------------------------- /packages/or-guard/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = { 3 | displayName: 'or-guard', 4 | preset: '../../jest.preset.js', 5 | globals: {}, 6 | testEnvironment: 'node', 7 | transform: { 8 | '^.+\\.[tj]sx?$': [ 9 | 'ts-jest', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 16 | coverageDirectory: '../../coverage/packages/or-guard', 17 | }; 18 | -------------------------------------------------------------------------------- /packages/or-guard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nest-lab/or-guard", 3 | "license": "MIT", 4 | "description": "A mixin guard that allows for one of N guards to pass for the request to be considers authenticated.", 5 | "author": { 6 | "name": "Jay McDoniel", 7 | "email": "me@jaymcdoniel.dev", 8 | "url": "https://github.com/jmcdo29" 9 | }, 10 | "version": "2.6.0", 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "main": "src/index.js", 15 | "files": [ 16 | "src" 17 | ], 18 | "repository": { 19 | "type": "github", 20 | "url": "https://github.com/jmcdo29/nest-lab", 21 | "directory": "packages/or-guard" 22 | }, 23 | "peerDependencies": { 24 | "@nestjs/common": " ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", 25 | "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", 26 | "rxjs": "^7.0.0" 27 | }, 28 | "peerDependenciesMeta": { 29 | "@nestjs/common": { 30 | "optional": false 31 | }, 32 | "@nestjs/core": { 33 | "optional": false 34 | }, 35 | "rxjs": { 36 | "optional": false 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/or-guard/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "or-guard", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/or-guard/src", 5 | "projectType": "library", 6 | "targets": { 7 | "lint": { 8 | "executor": "@nx/eslint:lint" 9 | }, 10 | "test": { 11 | "executor": "@nx/jest:jest", 12 | "outputs": ["{workspaceRoot}/coverage/packages/or-guard"], 13 | "options": { 14 | "jestConfig": "packages/or-guard/jest.config.ts" 15 | } 16 | }, 17 | "build": { 18 | "executor": "@nrwl/js:tsc", 19 | "outputs": ["{options.outputPath}"], 20 | "options": { 21 | "outputPath": "dist/packages/or-guard", 22 | "tsConfig": "packages/or-guard/tsconfig.lib.json", 23 | "packageJson": "packages/or-guard/package.json", 24 | "main": "packages/or-guard/src/index.ts", 25 | "assets": ["packages/or-guard/*.md"], 26 | "updateBuildableProjectDepsInPackageJson": true 27 | } 28 | }, 29 | "publish": { 30 | "executor": "nx:run-commands", 31 | "options": { 32 | "cwd": "dist/packages/or-guard", 33 | "command": "pnpm publish" 34 | }, 35 | "dependsOn": [ 36 | { 37 | "target": "build" 38 | } 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/or-guard/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/and.guard'; 2 | export * from './lib/or.guard'; 3 | -------------------------------------------------------------------------------- /packages/or-guard/src/lib/and.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Inject, 5 | InjectionToken, 6 | mixin, 7 | Type, 8 | } from '@nestjs/common'; 9 | import { ModuleRef } from '@nestjs/core'; 10 | import { 11 | defer, 12 | from, 13 | Observable, 14 | of, 15 | OperatorFunction, 16 | throwError, 17 | } from 'rxjs'; 18 | import { catchError, last, mergeMap, every, concatMap } from 'rxjs/operators'; 19 | 20 | interface AndGuardOptions { 21 | throwOnFirstError?: boolean; 22 | sequential?: boolean; 23 | } 24 | 25 | export function AndGuard( 26 | guards: Array | InjectionToken>, 27 | andGuardOptions?: AndGuardOptions 28 | ) { 29 | class AndMixinGuard implements CanActivate { 30 | private guards: CanActivate[] = []; 31 | constructor(@Inject(ModuleRef) private readonly modRef: ModuleRef) {} 32 | canActivate(context: ExecutionContext): Observable { 33 | this.guards = guards.map((guard) => 34 | this.modRef.get(guard, { strict: false }) 35 | ); 36 | const canActivateReturns: Array<() => Observable> = 37 | this.guards.map((guard) => () => this.deferGuard(guard, context)); 38 | const mapOperator = andGuardOptions?.sequential ? concatMap : mergeMap; 39 | return from(canActivateReturns).pipe( 40 | mapOperator((obs) => { 41 | return obs().pipe(this.handleError()); 42 | }), 43 | every((val) => val === true), 44 | last() 45 | ); 46 | } 47 | 48 | private deferGuard( 49 | guard: CanActivate, 50 | context: ExecutionContext 51 | ): Observable { 52 | return defer(() => { 53 | const guardVal = guard.canActivate(context); 54 | if (this.guardIsPromise(guardVal)) { 55 | return from(guardVal); 56 | } 57 | if (this.guardIsObservable(guardVal)) { 58 | return guardVal; 59 | } 60 | return of(guardVal); 61 | }); 62 | } 63 | 64 | private handleError(): OperatorFunction { 65 | return catchError((err) => { 66 | if (andGuardOptions?.throwOnFirstError) { 67 | return throwError(() => err); 68 | } 69 | return of(false); 70 | }); 71 | } 72 | 73 | private guardIsPromise( 74 | guard: boolean | Promise | Observable 75 | ): guard is Promise { 76 | return !!(guard as Promise).then; 77 | } 78 | 79 | private guardIsObservable( 80 | guard: boolean | Observable 81 | ): guard is Observable { 82 | return !!(guard as Observable).pipe; 83 | } 84 | } 85 | 86 | const Guard = mixin(AndMixinGuard); 87 | return Guard as Type; 88 | } 89 | -------------------------------------------------------------------------------- /packages/or-guard/src/lib/or.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Inject, 5 | InjectionToken, 6 | mixin, 7 | Type, 8 | } from '@nestjs/common'; 9 | import { ModuleRef } from '@nestjs/core'; 10 | import { 11 | concatMap, 12 | defer, 13 | from, 14 | map, 15 | Observable, 16 | of, 17 | OperatorFunction, 18 | throwError, 19 | pipe, tap 20 | } from 'rxjs'; 21 | import { catchError, last, mergeMap, takeWhile } from 'rxjs/operators'; 22 | 23 | interface OrGuardOptions { 24 | throwOnFirstError?: boolean; 25 | throwLastError?: boolean; 26 | throwError?: object | ((errors: unknown[]) => unknown) 27 | } 28 | 29 | export function OrGuard( 30 | guards: Array | InjectionToken>, 31 | orGuardOptions?: OrGuardOptions 32 | ) { 33 | class OrMixinGuard implements CanActivate { 34 | private guards: CanActivate[] = []; 35 | constructor(@Inject(ModuleRef) private readonly modRef: ModuleRef) {} 36 | canActivate(context: ExecutionContext): Observable { 37 | this.guards = guards.map((guard) => 38 | this.modRef.get(guard, { strict: false }) 39 | ); 40 | const canActivateReturns: Array> = this.guards.map( 41 | (guard) => this.deferGuard(guard, context) 42 | ); 43 | const errors: unknown[] = []; 44 | return from(canActivateReturns).pipe( 45 | mergeMap((obs) => obs.pipe(this.handleError())), 46 | tap(({ error }) => errors.push(error)), 47 | takeWhile(({ result }) => result === false, true), 48 | last(), 49 | concatMap(({ result }) => { 50 | if (result === false) { 51 | if (orGuardOptions?.throwLastError) { 52 | return throwError(() => errors.at(-1)) 53 | } 54 | 55 | if (orGuardOptions?.throwError) { 56 | return throwError(() => typeof orGuardOptions.throwError === 'function' ? orGuardOptions.throwError(errors) : orGuardOptions.throwError) 57 | } 58 | } 59 | 60 | return of(result); 61 | }) 62 | ); 63 | } 64 | 65 | private deferGuard( 66 | guard: CanActivate, 67 | context: ExecutionContext 68 | ): Observable { 69 | return defer(() => { 70 | const guardVal = guard.canActivate(context); 71 | if (this.guardIsPromise(guardVal)) { 72 | return from(guardVal); 73 | } 74 | if (this.guardIsObservable(guardVal)) { 75 | return guardVal; 76 | } 77 | return of(guardVal); 78 | }); 79 | } 80 | 81 | private handleError(): OperatorFunction { 82 | return pipe( 83 | catchError((error) => { 84 | if (orGuardOptions?.throwOnFirstError) { 85 | return throwError(() => error); 86 | } 87 | return of({ result: false, error }); 88 | }), 89 | map((result) => typeof result === 'boolean' ? { result } : result) 90 | ); 91 | } 92 | 93 | private guardIsPromise( 94 | guard: boolean | Promise | Observable 95 | ): guard is Promise { 96 | return !!(guard as Promise).then; 97 | } 98 | 99 | private guardIsObservable( 100 | guard: boolean | Observable 101 | ): guard is Observable { 102 | return !!(guard as Observable).pipe; 103 | } 104 | } 105 | 106 | const Guard = mixin(OrMixinGuard); 107 | return Guard as Type; 108 | } 109 | -------------------------------------------------------------------------------- /packages/or-guard/test/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, UnauthorizedException, UseGuards } from '@nestjs/common'; 2 | 3 | import { AndGuard, OrGuard } from '../src'; 4 | import { ObsGuard } from './obs.guard'; 5 | import { PromGuard } from './prom.guard'; 6 | import { ReadUserGuard } from './read-user.guard'; 7 | import { SetUserGuard } from './set-user.guard'; 8 | import { SyncGuard } from './sync.guard'; 9 | import { ThrowGuard } from './throw.guard'; 10 | 11 | @Controller() 12 | export class AppController { 13 | private message = 'Hello World'; 14 | @UseGuards(OrGuard([ObsGuard, PromGuard, SyncGuard])) 15 | @Get() 16 | getHello() { 17 | return this.message; 18 | } 19 | 20 | @UseGuards(OrGuard([ThrowGuard, SyncGuard])) 21 | @Get('do-not-throw') 22 | getThrowGuard() { 23 | return this.message; 24 | } 25 | 26 | @UseGuards(OrGuard([ThrowGuard, SyncGuard], { throwOnFirstError: true })) 27 | @Get('throw') 28 | getThrowGuardThrow() { 29 | return this.message; 30 | } 31 | 32 | @UseGuards(OrGuard([SyncGuard, ThrowGuard], { throwLastError: true })) 33 | @Get('throw-last') 34 | getThrowGuardThrowLast() { 35 | return this.message; 36 | } 37 | 38 | @UseGuards(OrGuard([ThrowGuard, ThrowGuard], { throwError: new UnauthorizedException('Should provide either "x-api-key" header or query') })) 39 | @Get('throw-custom') 40 | getThrowGuardThrowCustom() { 41 | return this.message; 42 | } 43 | 44 | @UseGuards(OrGuard([ThrowGuard, ThrowGuard], { throwError: (errors) => new UnauthorizedException((errors as { message?: string }[]).filter(error => error.message).join(', ')) })) 45 | @Get('throw-custom-narrow') 46 | getThrowGuardThrowCustomNarrow() { 47 | return this.message; 48 | } 49 | 50 | @UseGuards(OrGuard(['SyncAndProm', ObsGuard])) 51 | @Get('logical-and') 52 | getLogicalAnd() { 53 | return this.message; 54 | } 55 | 56 | @UseGuards(AndGuard([SetUserGuard, ReadUserGuard])) 57 | @Get('set-user-fail') 58 | getSetUserFail() { 59 | return this.message; 60 | } 61 | 62 | @UseGuards(AndGuard([SetUserGuard, ReadUserGuard], { sequential: true })) 63 | @Get('set-user-pass') 64 | getSetUserPass() { 65 | return this.message; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/or-guard/test/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AndGuard } from '../src/'; 4 | import { AppController } from './app.controller'; 5 | import { ObsGuard } from './obs.guard'; 6 | import { PromGuard } from './prom.guard'; 7 | import { SyncGuard } from './sync.guard'; 8 | import { ThrowGuard } from './throw.guard'; 9 | import { SetUserGuard } from './set-user.guard'; 10 | import { ReadUserGuard } from './read-user.guard'; 11 | 12 | @Module({ 13 | controllers: [AppController], 14 | providers: [ 15 | ObsGuard, 16 | SyncGuard, 17 | PromGuard, 18 | ThrowGuard, 19 | SetUserGuard, 20 | ReadUserGuard, 21 | { 22 | provide: 'SyncAndProm', 23 | useClass: AndGuard([SyncGuard, PromGuard]), 24 | }, 25 | ], 26 | }) 27 | export class AppModule {} 28 | -------------------------------------------------------------------------------- /packages/or-guard/test/obs.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; 2 | import { of, Observable } from 'rxjs'; 3 | 4 | @Injectable() 5 | export class ObsGuard implements CanActivate { 6 | canActivate(_context: ExecutionContext): Observable { 7 | return of(true); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/or-guard/test/or.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { TestingModuleBuilder, Test } from '@nestjs/testing'; 3 | import * as supertest from 'supertest'; 4 | import { of } from 'rxjs'; 5 | 6 | import { AppModule } from './app.module'; 7 | import { ObsGuard } from './obs.guard'; 8 | import { PromGuard } from './prom.guard'; 9 | import { SyncGuard } from './sync.guard'; 10 | 11 | describe('OrGuard and AndGuard Integration Test', () => { 12 | let moduleConfig: TestingModuleBuilder; 13 | 14 | beforeEach(() => { 15 | moduleConfig = Test.createTestingModule({ 16 | imports: [AppModule], 17 | }); 18 | }); 19 | 20 | describe.each` 21 | sync | syncExpect 22 | ${true} | ${true} 23 | ${false} | ${false} 24 | `( 25 | 'sync val $sync syncExpect $syncExpect', 26 | ({ sync, syncExpect }: { sync: boolean; syncExpect: boolean }) => { 27 | describe.each` 28 | prom | promExpect 29 | ${true} | ${true} 30 | ${false} | ${false} 31 | `( 32 | 'prom val $prom promExpect $promExpect', 33 | ({ prom, promExpect }: { prom: boolean; promExpect: boolean }) => { 34 | describe.each` 35 | obs | obsExpect 36 | ${true} | ${true} 37 | ${false} | ${syncExpect && promExpect} 38 | `( 39 | 'obs val $obs final expect $obsExpect', 40 | ({ obs, obsExpect }: { obs: boolean; obsExpect: boolean }) => { 41 | let app: INestApplication; 42 | beforeEach(async () => { 43 | const testMod = await moduleConfig 44 | .overrideProvider(SyncGuard) 45 | .useValue({ canActivate: () => sync }) 46 | .overrideProvider(PromGuard) 47 | .useValue({ canActivate: async () => prom }) 48 | .overrideProvider(ObsGuard) 49 | .useValue({ canActivate: () => of(obs) }) 50 | .compile(); 51 | app = testMod.createNestApplication(); 52 | await app.init(); 53 | }); 54 | afterEach(async () => { 55 | await app.close(); 56 | }); 57 | /** 58 | * OrGuard([AndGuard([SyncGuard, PromGuard]), ObsGuard]) 59 | * 60 | * | Sync | Prom | Obs | Final | 61 | * | - | - | - | - | 62 | * | true | true | true | true | 63 | * | true | true | false | true | 64 | * | true | false | true | true | 65 | * | true | false | false | false | 66 | * | false | true | true | true | 67 | * | false | true | false | false | 68 | * | false | false | true | true | 69 | * | false | false | false | false | 70 | */ 71 | it(`should make a request to the server and${ 72 | obsExpect ? ' ' : ' not ' 73 | }succeed`, async () => { 74 | return supertest(app.getHttpServer()) 75 | .get('/logical-and') 76 | .expect(obsExpect ? 200 : 403); 77 | }); 78 | } 79 | ); 80 | } 81 | ); 82 | describe.each` 83 | prom | promExpect 84 | ${true} | ${true} 85 | ${false} | ${syncExpect ?? false} 86 | `( 87 | 'prom val $prom promExpect $promExpect', 88 | ({ prom, promExpect }: { prom: boolean; promExpect: boolean }) => { 89 | describe.each` 90 | obs | obsExpect 91 | ${true} | ${true} 92 | ${false} | ${promExpect ?? false} 93 | `( 94 | 'obs val $obs final expect $obsExpect', 95 | ({ obs, obsExpect }: { obs: boolean; obsExpect: boolean }) => { 96 | let app: INestApplication; 97 | beforeEach(async () => { 98 | const testMod = await moduleConfig 99 | .overrideProvider(SyncGuard) 100 | .useValue({ canActivate: () => sync }) 101 | .overrideProvider(PromGuard) 102 | .useValue({ canActivate: async () => prom }) 103 | .overrideProvider(ObsGuard) 104 | .useValue({ canActivate: () => of(obs) }) 105 | .compile(); 106 | app = testMod.createNestApplication(); 107 | await app.init(); 108 | }); 109 | afterEach(async () => { 110 | await app.close(); 111 | }); 112 | /** 113 | * OrGuard([SyncGuard, PromGuard, ObsGuard]) 114 | * 115 | * | Sync | Prom | Obs | Final | 116 | * | - | - | - | - | 117 | * | true | true | true | true | 118 | * | true | true | false | true | 119 | * | true | false | true | true | 120 | * | true | false | false | true | 121 | * | false | true | true | true | 122 | * | false | true | false | true | 123 | * | false | false | true | true | 124 | * | false | false | false | false | 125 | */ 126 | it(`should make a request to the server and${ 127 | obsExpect ? ' ' : ' not ' 128 | }succeed`, async () => { 129 | return supertest(app.getHttpServer()) 130 | .get('/') 131 | .expect(obsExpect ? 200 : 403); 132 | }); 133 | } 134 | ); 135 | } 136 | ); 137 | describe('Using the throw guards', () => { 138 | let app: INestApplication; 139 | beforeEach(async () => { 140 | const testMod = await moduleConfig 141 | .overrideProvider(SyncGuard) 142 | .useValue({ canActivate: () => sync }) 143 | .compile(); 144 | app = testMod.createNestApplication(); 145 | await app.init(); 146 | }); 147 | afterEach(async () => { 148 | await app.close(); 149 | }); 150 | describe('do-not-throw', () => { 151 | /** 152 | * OrGuard([SyncGuard, ThrowGuard]) 153 | * 154 | * | Sync | Throw | Final | 155 | * | - | - | - | 156 | * | true | UnauthorizedException | true | 157 | * | false | UnauthorizedException | false | 158 | */ 159 | it(`should return with ${syncExpect ? 200 : 403}`, async () => { 160 | return supertest(app.getHttpServer()) 161 | .get('/do-not-throw') 162 | .expect(syncExpect ? 200 : 403); 163 | }); 164 | }); 165 | describe('throw', () => { 166 | /** 167 | * OrGuard([SyncGuard, ThrowGuard], { throwOnFirstError: true}) 168 | * 169 | * | Sync | Throw | Final | 170 | * | - | - | - | 171 | * | true | UnauthorizedException | false | 172 | * | false | UnauthorizedException | false | 173 | */ 174 | it('should throw an error regardless of syncExpect', async () => { 175 | return supertest(app.getHttpServer()) 176 | .get('/throw') 177 | .expect(401) 178 | .expect(({ body }) => { 179 | expect(body).toEqual( 180 | expect.objectContaining({ message: 'ThrowGuard' }) 181 | ); 182 | }); 183 | }); 184 | }); 185 | describe('throw-last', () => { 186 | /** 187 | * OrGuard([SyncGuard, ThrowGuard], { throwLastError: true }) 188 | * 189 | * | Sync | Throw | Final | 190 | * | - | - | - | 191 | * | true | UnauthorizedException | true | 192 | * | false | UnauthorizedException | UnauthorizedException | 193 | */ 194 | it('should throw the last error', async () => { 195 | return supertest(app.getHttpServer()) 196 | .get('/throw-last') 197 | .expect(sync ? 200 : 401) 198 | .expect(({ body }) => { 199 | if (!sync) { 200 | expect(body).toEqual( 201 | expect.objectContaining({ message: 'ThrowGuard' }) 202 | ); 203 | } 204 | }); 205 | }); 206 | }); 207 | describe('throw-custom', () => { 208 | /** 209 | * OrGuard([ThrowGuard, ThrowGuard], { throwError: new UnauthorizedException('Should provide either "x-api-key" header or query') }) 210 | * 211 | * | Throw | Throw | Final | 212 | * | - | - | - | 213 | * | UnauthorizedException | UnauthorizedException | object | 214 | * | UnauthorizedException | UnauthorizedException | object | 215 | */ 216 | it('should throw the custom error', async () => { 217 | return supertest(app.getHttpServer()) 218 | .get('/throw-custom') 219 | .expect(401) 220 | .expect(({ body }) => { 221 | expect(body).toEqual( 222 | expect.objectContaining({ message: 'Should provide either "x-api-key" header or query' }) 223 | ); 224 | }); 225 | }); 226 | }); 227 | describe('throw-custom-narrow', () => { 228 | /** 229 | * OrGuard([ThrowGuard, ThrowGuard], { throwError: (errors) => new UnauthorizedException((errors as { message?: string }[]).filter(error => error.message).join(', ')) }) 230 | * 231 | * | Throw | Throw | Final | 232 | * | - | - | - | 233 | * | UnauthorizedException | UnauthorizedException | UnauthorizedException | 234 | * | UnauthorizedException | UnauthorizedException | unknown | 235 | */ 236 | it('should throw the custom error', async () => { 237 | return supertest(app.getHttpServer()) 238 | .get('/throw-custom-narrow') 239 | .expect(401) 240 | .expect(({ body }) => { 241 | expect(body).toEqual( 242 | expect.objectContaining({ message: 'UnauthorizedException: ThrowGuard, UnauthorizedException: ThrowGuard' }) 243 | ); 244 | }); 245 | }); 246 | }); 247 | }); 248 | } 249 | ); 250 | 251 | describe('AndGuard with options', () => { 252 | let app: INestApplication; 253 | beforeAll(async () => { 254 | const testMod = await moduleConfig.compile(); 255 | app = testMod.createNestApplication(); 256 | await app.init(); 257 | }); 258 | afterAll(async () => { 259 | await app.close(); 260 | }); 261 | it('should throw an error if not ran sequentially', async () => { 262 | return supertest(app.getHttpServer()).get('/set-user-fail').expect(403); 263 | }); 264 | it('should not throw an error if ran sequentially', async () => { 265 | return supertest(app.getHttpServer()).get('/set-user-pass').expect(200); 266 | }); 267 | }); 268 | }); 269 | -------------------------------------------------------------------------------- /packages/or-guard/test/prom.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class PromGuard implements CanActivate { 5 | async canActivate(_context: ExecutionContext): Promise { 6 | return true; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/or-guard/test/read-user.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { Observable } from 'rxjs'; 3 | 4 | @Injectable() 5 | export class ReadUserGuard implements CanActivate { 6 | canActivate( 7 | context: ExecutionContext 8 | ): boolean | Promise | Observable { 9 | const req = context.switchToHttp().getRequest(); 10 | return !!req.user; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/or-guard/test/set-user.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { setTimeout } from 'timers/promises'; 3 | 4 | @Injectable() 5 | export class SetUserGuard implements CanActivate { 6 | async canActivate(context: ExecutionContext): Promise { 7 | await setTimeout(500); 8 | const req = context.switchToHttp().getRequest(); 9 | req.user = 'set'; 10 | return true; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/or-guard/test/sync.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class SyncGuard implements CanActivate { 5 | canActivate(_context: ExecutionContext): boolean { 6 | return true; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/or-guard/test/throw.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | CanActivate, 4 | ExecutionContext, 5 | UnauthorizedException, 6 | } from '@nestjs/common'; 7 | 8 | @Injectable() 9 | export class ThrowGuard implements CanActivate { 10 | canActivate(_context: ExecutionContext): boolean { 11 | throw new UnauthorizedException(ThrowGuard.name); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/or-guard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/or-guard/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"], 8 | "target": "es2021" 9 | }, 10 | "exclude": ["**/*.spec.ts", "jest.config.ts"], 11 | "include": ["src/**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/or-guard/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts", 14 | "jest.config.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["*.json"], 19 | "parser": "jsonc-eslint-parser", 20 | "rules": { 21 | "@nx/dependency-checks": "error" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @nest-lab/throttler-storage-redis 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - 45c426c: Now supporting NestJS v11 8 | 9 | ## 1.0.0 10 | 11 | ### Major Changes 12 | 13 | - 66bccec: Initial release of the @nest-lab/throttler-storage-redis package 14 | 15 | This package was 16 | [initially maintained by kkoomen](https://github.com/kkoomen/nestjs-throttler-storage-redis), 17 | but has since been brought into the `@nest-lab/` repository for management. 18 | Nothing about the usage of the module has changed, other than a slight 19 | variation of the name. 20 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/README.md: -------------------------------------------------------------------------------- 1 | # NestJS Throttler Redis Storage 2 | 3 | Redis storage provider for the 4 | [@nestjs/throttler](https://github.com/nestjs/throttler) package. 5 | 6 | # Installation 7 | 8 | ### Yarn 9 | 10 | - `yarn add @nest-lab/throttler-storage-redis ioredis` 11 | 12 | ### NPM 13 | 14 | - `npm install --save @nest-lab/throttler-storage-redis ioredis` 15 | 16 | # Usage 17 | 18 | Basic usage: 19 | 20 | ```ts 21 | import { ThrottlerModule, seconds } from '@nestjs/throttler'; 22 | import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'; 23 | import Redis from 'ioredis'; 24 | 25 | @Module({ 26 | imports: [ 27 | ThrottlerModule.forRoot({ 28 | throttlers: [{ limit: 5, ttl: seconds(60) }], 29 | 30 | // Below are possible options on how to configure the storage service. 31 | 32 | // default config (host = localhost, port = 6379) 33 | storage: new ThrottlerStorageRedisService(), 34 | 35 | // connection url 36 | storage: new ThrottlerStorageRedisService('redis://'), 37 | 38 | // redis object 39 | storage: new ThrottlerStorageRedisService(new Redis()), 40 | 41 | // redis clusters 42 | storage: new ThrottlerStorageRedisService( 43 | new Redis.Cluster(nodes, options) 44 | ), 45 | }), 46 | ], 47 | }) 48 | export class AppModule {} 49 | ``` 50 | 51 | Inject another config module and service: 52 | 53 | ```ts 54 | import { ThrottlerModule } from '@nestjs/throttler'; 55 | import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis'; 56 | 57 | @Module({ 58 | imports: [ 59 | ThrottlerModule.forRootAsync({ 60 | imports: [ConfigModule], 61 | inject: [ConfigService], 62 | useFactory: (config: ConfigService) => ({ 63 | throttlers: [ 64 | { 65 | ttl: config.get('THROTTLE_TTL'), 66 | limit: config.get('THROTTLE_LIMIT'), 67 | }, 68 | ], 69 | storage: new ThrottlerStorageRedisService(), 70 | }), 71 | }), 72 | ], 73 | }) 74 | export class AppModule {} 75 | ``` 76 | 77 | # Issues 78 | 79 | Bugs and features related to the redis implementation are welcome in this 80 | repository. 81 | 82 | # License 83 | 84 | NestJS Throttler Redis Storage is licensed under the MIT license. 85 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis 4 | container_name: redis 5 | ports: 6 | - '6379:6379' 7 | 8 | redis_cluster: 9 | container_name: redis_cluster 10 | image: grokzen/redis-cluster:7.0.10 11 | environment: 12 | - 'IP=0.0.0.0' 13 | ports: 14 | - '7000-7050:7000-7050' 15 | - '5000-5010:5000-5010' 16 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'throttler-storage-redis', 4 | preset: '../../jest.preset.js', 5 | testEnvironment: 'node', 6 | transform: { 7 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], 8 | }, 9 | moduleFileExtensions: ['ts', 'js', 'html'], 10 | coverageDirectory: '../../coverage/packages/throttler-storage-redis', 11 | }; 12 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nest-lab/throttler-storage-redis", 3 | "description": "Redis storage provider for the @nestjs/throttler package", 4 | "version": "1.1.0", 5 | "keywords": [ 6 | "nestjs", 7 | "rate-limit", 8 | "throttle", 9 | "express", 10 | "fastify", 11 | "redis" 12 | ], 13 | "repository": { 14 | "type": "github", 15 | "url": "https://github.com/jmcdo29/nest-lab", 16 | "directory": "packages/throttler-storage-redis" 17 | }, 18 | "private": false, 19 | "license": "MIT", 20 | "dependencies": { 21 | "tslib": "^2.3.0" 22 | }, 23 | "devDependencies": { 24 | "ioredis": "5.4.1" 25 | }, 26 | "peerDependencies": { 27 | "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", 28 | "@nestjs/core": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", 29 | "@nestjs/throttler": ">=6.0.0", 30 | "ioredis": ">=5.0.0", 31 | "reflect-metadata": "^0.2.1" 32 | }, 33 | "peerDependenciesMeta": { 34 | "@nestjs/common": { 35 | "optional": false 36 | }, 37 | "@nestjs/core": { 38 | "optional": false 39 | }, 40 | "@nestjs/throttler": { 41 | "optional": false 42 | }, 43 | "ioredis": { 44 | "optional": false 45 | }, 46 | "reflect-metadata": { 47 | "optional": false 48 | } 49 | }, 50 | "type": "commonjs", 51 | "main": "./src/index.js", 52 | "typings": "./src/index.d.ts" 53 | } 54 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "throttler-storage-redis", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/throttler-storage-redis/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "build": { 9 | "executor": "@nx/js:tsc", 10 | "outputs": ["{options.outputPath}"], 11 | "options": { 12 | "outputPath": "dist/packages/throttler-storage-redis", 13 | "tsConfig": "packages/throttler-storage-redis/tsconfig.lib.json", 14 | "packageJson": "packages/throttler-storage-redis/package.json", 15 | "main": "packages/throttler-storage-redis/src/index.ts", 16 | "assets": ["packages/throttler-storage-redis/*.md"] 17 | } 18 | }, 19 | "publish": { 20 | "executor": "nx:run-commands", 21 | "options": { 22 | "cwd": "dist/packages/throttler-storage-redis", 23 | "command": "pnpm publish" 24 | }, 25 | "dependsOn": [ 26 | { 27 | "target": "build" 28 | } 29 | ] 30 | }, 31 | "lint": { 32 | "executor": "@nx/eslint:lint" 33 | }, 34 | "start-docker": { 35 | "executor": "nx:run-commands", 36 | "options": { 37 | "cwd": "packages/throttler-storage-redis", 38 | "command": "docker compose up -d" 39 | }, 40 | "cache": false 41 | }, 42 | "wait-for-docker": { 43 | "executor": "nx:run-commands", 44 | "options": { 45 | "commands": ["nc -vzw 5000 localhost 5000-5010 6379 7000-7050"], 46 | "parallel": true 47 | }, 48 | "cache": false, 49 | "dependsOn": ["start-docker"] 50 | }, 51 | "run-tests": { 52 | "executor": "@nx/jest:jest", 53 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 54 | "options": { 55 | "jestConfig": "packages/throttler-storage-redis/jest.config.ts" 56 | }, 57 | "dependsOn": ["wait-for-docker"] 58 | }, 59 | "test": { 60 | "executor": "nx:run-commands", 61 | "options": { 62 | "cwd": "packages/throttler-storage-redis", 63 | "command": "docker compose down" 64 | }, 65 | "cache": false, 66 | "dependsOn": ["run-tests"] 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './throttler-storage-redis.interface'; 2 | export * from './throttler-storage-redis.service'; 3 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/src/throttler-storage-redis.interface.ts: -------------------------------------------------------------------------------- 1 | import { ThrottlerStorage } from '@nestjs/throttler'; 2 | import Redis, { Cluster } from 'ioredis'; 3 | 4 | export interface ThrottlerStorageRedis { 5 | /** 6 | * The redis instance. 7 | */ 8 | redis: Redis | Cluster; 9 | 10 | /** 11 | * Increment the amount of requests for a given record. The record will 12 | * automatically be removed from the storage once its TTL has been reached. 13 | */ 14 | increment: ThrottlerStorage['increment']; 15 | } 16 | 17 | export const ThrottlerStorageRedis = Symbol('ThrottlerStorageRedis'); 18 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/src/throttler-storage-redis.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleDestroy } from '@nestjs/common'; 2 | import { ThrottlerStorageRecord } from '@nestjs/throttler/dist/throttler-storage-record.interface'; 3 | import Redis, { Cluster, RedisOptions } from 'ioredis'; 4 | import { ThrottlerStorageRedis } from './throttler-storage-redis.interface'; 5 | 6 | @Injectable() 7 | export class ThrottlerStorageRedisService implements ThrottlerStorageRedis, OnModuleDestroy { 8 | scriptSrc: string; 9 | redis: Redis | Cluster; 10 | disconnectRequired?: boolean; 11 | 12 | constructor(redis?: Redis); 13 | constructor(cluster?: Cluster); 14 | constructor(options?: RedisOptions); 15 | constructor(url?: string); 16 | constructor(redisOrOptions?: Redis | Cluster | RedisOptions | string) { 17 | if (redisOrOptions instanceof Redis || redisOrOptions instanceof Cluster) { 18 | this.redis = redisOrOptions; 19 | } else if (typeof redisOrOptions === 'string') { 20 | this.redis = new Redis(redisOrOptions as string); 21 | this.disconnectRequired = true; 22 | } else { 23 | this.redis = new Redis(redisOrOptions as RedisOptions); 24 | this.disconnectRequired = true; 25 | } 26 | 27 | this.scriptSrc = this.getScriptSrc(); 28 | } 29 | 30 | getScriptSrc(): string { 31 | // Credits to wyattjoh for the fast implementation you see below. 32 | // https://github.com/wyattjoh/rate-limit-redis/blob/main/src/lib.ts 33 | return ` 34 | local hitKey = KEYS[1] 35 | local blockKey = KEYS[2] 36 | local throttlerName = ARGV[1] 37 | local ttl = tonumber(ARGV[2]) 38 | local limit = tonumber(ARGV[3]) 39 | local blockDuration = tonumber(ARGV[4]) 40 | 41 | local totalHits = redis.call('INCR', hitKey) 42 | local timeToExpire = redis.call('PTTL', hitKey) 43 | 44 | if timeToExpire <= 0 then 45 | redis.call('PEXPIRE', hitKey, ttl) 46 | timeToExpire = ttl 47 | end 48 | 49 | local isBlocked = redis.call('GET', blockKey) 50 | local timeToBlockExpire = 0 51 | 52 | if isBlocked then 53 | timeToBlockExpire = redis.call('PTTL', blockKey) 54 | elseif totalHits > limit then 55 | redis.call('SET', blockKey, 1, 'PX', blockDuration) 56 | isBlocked = '1' 57 | timeToBlockExpire = blockDuration 58 | end 59 | 60 | if isBlocked and timeToBlockExpire <= 0 then 61 | redis.call('DEL', blockKey) 62 | redis.call('SET', hitKey, 1, 'PX', ttl) 63 | totalHits = 1 64 | timeToExpire = ttl 65 | isBlocked = false 66 | end 67 | 68 | return { totalHits, timeToExpire, isBlocked and 1 or 0, timeToBlockExpire } 69 | ` 70 | .replace(/^\s+/gm, '') 71 | .trim(); 72 | } 73 | 74 | async increment( 75 | key: string, 76 | ttl: number, 77 | limit: number, 78 | blockDuration: number, 79 | throttlerName: string, 80 | ): Promise { 81 | const hitKey = `${this.redis.options.keyPrefix}{${key}:${throttlerName}}:hits`; 82 | const blockKey = `${this.redis.options.keyPrefix}{${key}:${throttlerName}}:blocked`; 83 | const results: number[] = (await this.redis.call( 84 | 'EVAL', 85 | this.scriptSrc, 86 | 2, 87 | hitKey, 88 | blockKey, 89 | throttlerName, 90 | ttl, 91 | limit, 92 | blockDuration, 93 | )) as number[]; 94 | 95 | if (!Array.isArray(results)) { 96 | throw new TypeError(`Expected result to be array of values, got ${results}`); 97 | } 98 | 99 | const [totalHits, timeToExpire, isBlocked, timeToBlockExpire] = results; 100 | 101 | if (typeof totalHits !== 'number') { 102 | throw new TypeError('Expected totalHits to be a number'); 103 | } 104 | 105 | if (typeof timeToExpire !== 'number') { 106 | throw new TypeError('Expected timeToExpire to be a number'); 107 | } 108 | 109 | if (typeof isBlocked !== 'number') { 110 | throw new TypeError('Expected isBlocked to be a number'); 111 | } 112 | 113 | if (typeof timeToBlockExpire !== 'number') { 114 | throw new TypeError('Expected timeToBlockExpire to be a number'); 115 | } 116 | 117 | return { 118 | totalHits, 119 | timeToExpire: Math.ceil(timeToExpire / 1000), 120 | isBlocked: isBlocked === 1, 121 | timeToBlockExpire: Math.ceil(timeToBlockExpire / 1000), 122 | }; 123 | } 124 | 125 | onModuleDestroy() { 126 | if (this.disconnectRequired) { 127 | this.redis?.disconnect(false); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/src/type.ts: -------------------------------------------------------------------------------- 1 | import { ThrottlerStorageRedis } from './throttler-storage-redis.interface'; 2 | 3 | export type Type = { new (...args: any[]): T }; 4 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { APP_GUARD } from '@nestjs/core'; 3 | import { ThrottlerGuard } from '@nestjs/throttler'; 4 | // import { ClusterControllerModule } from './controllers/cluster-controller.module'; 5 | import { ControllerModule } from './controllers/controller.module'; 6 | 7 | @Module({ 8 | imports: [ControllerModule], 9 | // imports: [ClusterControllerModule], 10 | providers: [ 11 | { 12 | provide: APP_GUARD, 13 | useClass: ThrottlerGuard, 14 | }, 15 | ], 16 | }) 17 | export class AppModule {} 18 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | success() { 6 | return { success: true }; 7 | } 8 | 9 | ignored() { 10 | return { ignored: true }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/app/controllers/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { SkipThrottle, Throttle, seconds } from '@nestjs/throttler'; 3 | import { AppService } from '../app.service'; 4 | 5 | @Controller() 6 | @Throttle({ default: { limit: 2, ttl: seconds(10) } }) 7 | export class AppController { 8 | constructor(private readonly appService: AppService) {} 9 | 10 | @Get() 11 | async test() { 12 | return this.appService.success(); 13 | } 14 | 15 | @Get('ignored') 16 | @SkipThrottle() 17 | async ignored() { 18 | return this.appService.ignored(); 19 | } 20 | 21 | @Get('ignore-user-agents') 22 | async ignoreUserAgents() { 23 | return this.appService.ignored(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/app/controllers/cluster-controller.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ThrottlerModule, seconds } from '@nestjs/throttler'; 3 | import { ThrottlerStorageRedisService } from '../../../src'; 4 | import { cluster } from '../../utility/redis-cluster'; 5 | import { AppService } from '../app.service'; 6 | import { AppController } from './app.controller'; 7 | import { DefaultController } from './default.controller'; 8 | import { LimitController } from './limit.controller'; 9 | 10 | @Module({ 11 | imports: [ 12 | ThrottlerModule.forRoot({ 13 | throttlers: [{ limit: 5, ttl: seconds(60) }], 14 | ignoreUserAgents: [/throttler-test/g], 15 | storage: new ThrottlerStorageRedisService(cluster), 16 | }), 17 | ], 18 | controllers: [AppController, DefaultController, LimitController], 19 | providers: [AppService], 20 | }) 21 | export class ClusterControllerModule {} 22 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/app/controllers/controller.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ThrottlerModule, seconds } from '@nestjs/throttler'; 3 | import { ThrottlerStorageRedisService } from '../../../src'; 4 | import { redis } from '../../utility/redis'; 5 | import { AppService } from '../app.service'; 6 | import { AppController } from './app.controller'; 7 | import { DefaultController } from './default.controller'; 8 | import { LimitController } from './limit.controller'; 9 | 10 | @Module({ 11 | imports: [ 12 | ThrottlerModule.forRoot({ 13 | throttlers: [{ limit: 5, ttl: seconds(60) }], 14 | ignoreUserAgents: [/throttler-test/g], 15 | storage: new ThrottlerStorageRedisService(redis), 16 | }), 17 | ], 18 | controllers: [AppController, DefaultController, LimitController], 19 | providers: [AppService], 20 | }) 21 | export class ControllerModule {} 22 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/app/controllers/default.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from '../app.service'; 3 | 4 | @Controller('default') 5 | export class DefaultController { 6 | constructor(private readonly appService: AppService) {} 7 | @Get() 8 | getDefault() { 9 | return this.appService.success(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/app/controllers/limit.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { Throttle, seconds } from '@nestjs/throttler'; 3 | import { AppService } from '../app.service'; 4 | 5 | @Throttle({ default: { limit: 2, ttl: seconds(10) } }) 6 | @Controller('limit') 7 | export class LimitController { 8 | constructor(private readonly appService: AppService) {} 9 | @Get() 10 | getThrottled() { 11 | return this.appService.success(); 12 | } 13 | 14 | @Throttle({ default: { limit: 5, ttl: seconds(10) } }) 15 | @Get('higher') 16 | getHigher() { 17 | return this.appService.success(); 18 | } 19 | 20 | @Throttle({ default: { limit: 3, ttl: seconds(10) } }) 21 | @Get('flooded') 22 | getFlooded() { 23 | return this.appService.success(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/app/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { ExpressAdapter } from '@nestjs/platform-express'; 3 | import { AppModule } from './app.module'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create( 7 | AppModule, 8 | new ExpressAdapter(), 9 | // new FastifyAdapter(), 10 | ); 11 | await app.listen(3000); 12 | } 13 | bootstrap(); 14 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, ModuleMetadata } from '@nestjs/common'; 2 | import { AbstractHttpAdapter, APP_GUARD } from '@nestjs/core'; 3 | import { ExpressAdapter } from '@nestjs/platform-express'; 4 | import { FastifyAdapter } from '@nestjs/platform-fastify'; 5 | import { Test, TestingModule } from '@nestjs/testing'; 6 | import { ThrottlerGuard } from '@nestjs/throttler'; 7 | import Redis, { Cluster } from 'ioredis'; 8 | import { ClusterControllerModule } from './app/controllers/cluster-controller.module'; 9 | import { ControllerModule } from './app/controllers/controller.module'; 10 | import { httPromise } from './utility/httpromise'; 11 | import { redis } from './utility/redis'; 12 | import { cluster } from './utility/redis-cluster'; 13 | 14 | async function flushdb(redisOrCluster: Redis | Cluster) { 15 | if (redisOrCluster instanceof Redis) { 16 | await redisOrCluster.flushall(); 17 | } else { 18 | // cluster instance 19 | await Promise.all( 20 | redisOrCluster.nodes('master').map(function (node) { 21 | return node.flushall(); 22 | }) 23 | ); 24 | } 25 | } 26 | 27 | describe.each` 28 | instance | instanceType 29 | ${redis} | ${'single'} 30 | ${cluster} | ${'cluster'} 31 | `( 32 | 'Redis $instanceType instance', 33 | ({ instance: redisOrCluster }: { instance: Redis | Cluster }) => { 34 | afterAll(async () => { 35 | await redisOrCluster.quit(); 36 | }); 37 | 38 | describe.each` 39 | adapter | adapterName 40 | ${new ExpressAdapter()} | ${'Express'} 41 | ${new FastifyAdapter()} | ${'Fastify'} 42 | `( 43 | '$adapterName Throttler', 44 | ({ adapter }: { adapter: AbstractHttpAdapter }) => { 45 | let app: INestApplication; 46 | 47 | beforeAll(async () => { 48 | await flushdb(redisOrCluster); 49 | const config: ModuleMetadata = { 50 | imports: [], 51 | providers: [ 52 | { 53 | provide: APP_GUARD, 54 | useClass: ThrottlerGuard, 55 | }, 56 | ], 57 | }; 58 | 59 | if (redisOrCluster instanceof Cluster) { 60 | config.imports?.push(ClusterControllerModule); 61 | } else { 62 | config.imports?.push(ControllerModule); 63 | } 64 | 65 | const moduleFixture: TestingModule = await Test.createTestingModule( 66 | config 67 | ).compile(); 68 | app = moduleFixture.createNestApplication(adapter); 69 | await app.listen(0); 70 | }); 71 | 72 | afterAll(async () => { 73 | await app.close(); 74 | }); 75 | 76 | describe('controllers', () => { 77 | let appUrl: string; 78 | beforeAll(async () => { 79 | appUrl = await app.getUrl(); 80 | }); 81 | 82 | /** 83 | * Tests for setting `@Throttle()` at the method level and for ignore routes 84 | */ 85 | describe('AppController', () => { 86 | it('GET /ignored', async () => { 87 | const response = await httPromise(appUrl + '/ignored'); 88 | expect(response.data).toEqual({ ignored: true }); 89 | expect(response.headers).not.toMatchObject({ 90 | 'x-ratelimit-limit': '2', 91 | 'x-ratelimit-remaining': '1', 92 | 'x-ratelimit-reset': '10', 93 | }); 94 | }); 95 | it('GET /ignore-user-agents', async () => { 96 | const response = await httPromise( 97 | appUrl + '/ignore-user-agents', 98 | 'GET', 99 | { 100 | 'user-agent': 'throttler-test/0.0.0', 101 | } 102 | ); 103 | expect(response.data).toEqual({ ignored: true }); 104 | expect(response.headers).not.toMatchObject({ 105 | 'x-ratelimit-limit': '2', 106 | 'x-ratelimit-remaining': '1', 107 | 'x-ratelimit-reset': '10', 108 | }); 109 | }); 110 | it('GET /', async () => { 111 | const response = await httPromise(appUrl + '/'); 112 | expect(response.data).toEqual({ success: true }); 113 | expect(response.headers).toMatchObject({ 114 | 'x-ratelimit-limit': '2', 115 | 'x-ratelimit-remaining': '1', 116 | 'x-ratelimit-reset': '10', 117 | }); 118 | }); 119 | }); 120 | /** 121 | * Tests for setting `@Throttle()` at the class level and overriding at the method level 122 | */ 123 | describe('LimitController', () => { 124 | it.each` 125 | method | url | limit 126 | ${'GET'} | ${''} | ${2} 127 | ${'GET'} | ${'/higher'} | ${5} 128 | `( 129 | '$method $url', 130 | async ({ 131 | method, 132 | url, 133 | limit, 134 | }: { 135 | method: 'GET'; 136 | url: string; 137 | limit: number; 138 | }) => { 139 | for (let i = 0; i < limit; i++) { 140 | const response = await httPromise( 141 | appUrl + '/limit' + url, 142 | method 143 | ); 144 | expect(response.data).toEqual({ success: true }); 145 | expect(response.headers).toMatchObject({ 146 | 'x-ratelimit-limit': limit.toString(), 147 | 'x-ratelimit-remaining': (limit - (i + 1)).toString(), 148 | 'x-ratelimit-reset': '10', 149 | }); 150 | } 151 | const errRes = await httPromise( 152 | appUrl + '/limit' + url, 153 | method 154 | ); 155 | expect(errRes.data).toMatchObject({ 156 | statusCode: 429, 157 | message: /ThrottlerException/, 158 | }); 159 | expect(errRes.headers).toMatchObject({ 160 | 'retry-after': '10', 161 | }); 162 | expect(errRes.status).toBe(429); 163 | } 164 | ); 165 | 166 | it('GET /flooded', async () => { 167 | // Try to flood an endpoint with a lot of requests and check if no 168 | // more than the given limit are able to bypass. 169 | const limit = 3; 170 | for (let i = 0; i < 200; i++) { 171 | const response = await httPromise( 172 | appUrl + '/limit/flooded', 173 | 'GET' 174 | ); 175 | if (i < limit) { 176 | expect(response.data).toEqual({ success: true }); 177 | expect( 178 | parseInt(response.headers['x-ratelimit-reset']) 179 | ).toBeLessThanOrEqual(10); 180 | expect(response.headers).toMatchObject({ 181 | 'x-ratelimit-limit': limit.toString(), 182 | 'x-ratelimit-remaining': (limit - (i + 1)).toString(), 183 | 'x-ratelimit-reset': /^\d+$/, 184 | }); 185 | } else { 186 | expect(response.data).toMatchObject({ 187 | statusCode: 429, 188 | message: /ThrottlerException/, 189 | }); 190 | expect( 191 | parseInt(response.headers['retry-after']) 192 | ).toBeLessThanOrEqual(10); 193 | expect(response.status).toBe(429); 194 | } 195 | } 196 | }); 197 | }); 198 | /** 199 | * Tests for setting throttle values at the `forRoot` level 200 | */ 201 | describe('DefaultController', () => { 202 | it('GET /default', async () => { 203 | const response = await httPromise(appUrl + '/default'); 204 | expect(response.data).toEqual({ success: true }); 205 | expect(response.headers).toMatchObject({ 206 | 'x-ratelimit-limit': '5', 207 | 'x-ratelimit-remaining': '4', 208 | 'x-ratelimit-reset': '60', 209 | }); 210 | }); 211 | }); 212 | }); 213 | } 214 | ); 215 | } 216 | ); 217 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": "..", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/utility/httpromise.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'http'; 2 | 3 | type HttpMethods = 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT'; 4 | 5 | export function httPromise( 6 | url: string, 7 | method: HttpMethods = 'GET', 8 | headers: Record = {}, 9 | body?: Record 10 | ): Promise<{ data: any; headers: Record; status: number }> { 11 | return new Promise((resolve, reject) => { 12 | const req = request(url, (res) => { 13 | res.setEncoding('utf-8'); 14 | let data = ''; 15 | res.on('data', (chunk) => { 16 | data += chunk; 17 | }); 18 | res.on('end', () => { 19 | return resolve({ 20 | data: JSON.parse(data), 21 | headers: res.headers, 22 | status: res.statusCode ?? 500, 23 | }); 24 | }); 25 | res.on('error', (err) => { 26 | return reject({ 27 | data: err, 28 | headers: res.headers, 29 | status: res.statusCode, 30 | }); 31 | }); 32 | }); 33 | req.method = method; 34 | 35 | Object.keys(headers).forEach((key) => { 36 | req.setHeader(key, headers[key]); 37 | }); 38 | 39 | switch (method) { 40 | case 'GET': 41 | break; 42 | case 'POST': 43 | case 'PUT': 44 | case 'PATCH': 45 | req.setHeader('Content-Type', 'application/json'); 46 | req.setHeader( 47 | 'Content-Length', 48 | Buffer.byteLength(Buffer.from(JSON.stringify(body))) 49 | ); 50 | req.write(Buffer.from(JSON.stringify(body))); 51 | break; 52 | case 'DELETE': 53 | break; 54 | default: 55 | reject(new Error('Invalid HTTP method')); 56 | break; 57 | } 58 | req.end(); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/utility/redis-cluster.ts: -------------------------------------------------------------------------------- 1 | import { Cluster } from 'ioredis'; 2 | 3 | export const clusterNodes = [ 4 | { host: '127.0.0.1', port: 7000 }, 5 | { host: '127.0.0.1', port: 7001 }, 6 | { host: '127.0.0.1', port: 7002 }, 7 | { host: '127.0.0.1', port: 7003 }, 8 | { host: '127.0.0.1', port: 7004 }, 9 | { host: '127.0.0.1', port: 7005 }, 10 | ]; 11 | 12 | export const cluster = new Cluster(clusterNodes); 13 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/test/utility/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis'; 2 | 3 | export const redis = new Redis(); 4 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | }, 18 | { 19 | "path": "./tsconfig.spec.json" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declaration": true, 6 | "types": ["node"], 7 | "target": "es2021", 8 | "strictNullChecks": true, 9 | "noImplicitAny": true, 10 | "strictBindCallApply": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true 13 | }, 14 | "include": ["src/**/*.ts"], 15 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/throttler-storage-redis/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "jest.config.ts", 10 | "test/**/*.test.ts", 11 | "test/**/*.spec.ts", 12 | "test/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/typeschema/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /packages/typeschema/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @nest-lab/typeschema 2 | 3 | ## 1.1.0 4 | 5 | ### Minor Changes 6 | 7 | - 45c426c: Now supporting NestJS v11 8 | 9 | ## 1.0.0 10 | 11 | ### Major Changes 12 | 13 | - ba2665d: Update typeschema to @typeschema/main and allow for the usage of all 14 | underlying adapters 15 | 16 | ## 0.2.1 17 | 18 | ### Patch Changes 19 | 20 | - 482b43f: fix: make `options` optional 21 | 22 | ## 0.2.0 23 | 24 | ### Minor Changes 25 | 26 | - 5e274ec: Add a new options parameter to the typeschema validation pipe 27 | 28 | BREAKING CHANGE: the logger is now the **second** parameter of the validation 29 | pipe with the options being the first. If you use `new ValidationPipe` and 30 | pass in the logger, you'll need to pass in an empty object or `undefined` as 31 | the first parameter. 32 | 33 | ## 0.1.1 34 | 35 | ### Patch Changes 36 | 37 | - 6e0e4b4: Reflect the type of the input schema via generics and an explicit 38 | type reference. Types should no longer show up as `unknown` 39 | 40 | ## 0.1.0 41 | 42 | ### Minor Changes 43 | 44 | - 68104f1: Initial release of @nest-lab/typeschema 45 | -------------------------------------------------------------------------------- /packages/typeschema/README.md: -------------------------------------------------------------------------------- 1 | # @nest-lab/typeschema 2 | 3 | [Typeschema][typeschema] is a general purpose validator that integrates with 4 | _several_ other validation packages. This library brings that integration into 5 | [NestJS][nest] as well! Just as Nest by default can integrate with 6 | class-validator, this package provides a `ValidationPipe` to bring a similar 7 | experience to `@decs/typeschema`. 8 | 9 | ## Crafting DTOs 10 | 11 | As all of these validation libraries typeschema integrates with use schemas 12 | instead of classes, we need to do a little work creating the DTO classes for 13 | Typescript's metadata reflection to enable us to have the same experience that 14 | already exists. Fortunately, this is pretty straightforward with how typeschema 15 | works! Simple extend the `TypeschemaDto` mixin by passing it your proper schema 16 | and viola! the DTO has been made. 17 | 18 | ```typescript 19 | import { TypeschemaDto } from '@nest-lab/typeschema'; 20 | import { z } from 'zod'; 21 | 22 | const helloWorldSchema = z.object({ 23 | message: z.string(), 24 | }); 25 | 26 | export class HelloWorldDto extends TypeschemaDto(helloWorldSchema) {} 27 | ``` 28 | 29 | What this does under the hood is creates a class that has a `static schema` 30 | property, along with a few others, that are then used by the 31 | `ValidationPipe` so that the incoming data can be properly validated. 32 | 33 | ## Consuming the Data from the ValidationPipe 34 | 35 | Something to make sure to take note of, this `ValidationPipe` **does not** 36 | return the exact same data as was passed in. In other words, unlike Nest's 37 | default `ValidationPipe`, this one does change the structure of the data after 38 | it has been parsed. It does this to provide a sane, type safe API. For 39 | **every** `TypeschemaDto` class, to access the data after it has been validated 40 | by the pipe, simply access the `.data` property. 41 | 42 | ```typescript 43 | @Controller() 44 | export class AppController { 45 | @Post('ajv') 46 | ajvTest(@Body() body: AjvDto) { 47 | return body.data; 48 | } 49 | } 50 | ``` 51 | 52 | ## Binding the ValidationPipe 53 | 54 | Just like the existing validation pipe, you can bind this pipe using 55 | `APP_PIPE` or `app.useGlobalPipes(new ValidationPipe())` for globally binding 56 | it, or simple `@UsePipes(ValidationPipe)` to bind it to a controller or method, 57 | or `@Body(ValidationPipe)` to bind it to a single parameter. 58 | 59 | ## Options for the pipe 60 | 61 | Currently the only option is an `exceptionFactory` that takes in an array of 62 | `ValidationIssue` from the `@decs/typeschema` package and returns an `Error` to 63 | be thrown. By default, this array is passed directly to `BadRequestException` 64 | from `@nestjs/common`. 65 | 66 | If you want to pass the options you can via `new ValidationPipe`, or if you 67 | prefer to let Nest inject the options you can add them via the `TypeschemaOptions` 68 | injection token using a custom provider. 69 | 70 | ## Integration with OpenAPI 71 | 72 | I know this is going to be a big question, and eventually I want to have the 73 | integration automatically there. However, right now, because of the underlying 74 | libraries, there's no immediate introspection of each library, which means that 75 | we can't, in a type safe manner, create the OpenAPI spec for each DTO. 76 | 77 | However, all is not lost, as it _can_ still integrate with `@nestjs/swagger` so 78 | long as you add a `static OPENAPI_METADATA` property to the DTO. For the format 79 | of this, please check [`@nestjs/swagger`'s integration tests][swaggertests]. 80 | 81 | [typeschema]: https://typeschema.com/ 82 | [nest]: https://docs.nestjs.com/ 83 | [swaggertests]: https://github.com/nestjs/swagger/tree/master/test/plugin/fixtures 84 | -------------------------------------------------------------------------------- /packages/typeschema/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'typescehma', 4 | preset: '../../jest.preset.js', 5 | testEnvironment: 'node', 6 | transform: { 7 | '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], 8 | }, 9 | moduleFileExtensions: ['ts', 'js', 'html'], 10 | }; 11 | -------------------------------------------------------------------------------- /packages/typeschema/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nest-lab/typeschema", 3 | "author": { 4 | "name": "Jay McDoniel", 5 | "email": "me@jaymcdoniel.dev", 6 | "url": "https://github.com/jmcdo29" 7 | }, 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "repository": { 12 | "type": "github", 13 | "url": "https://github.com/jmcdo29/nest-lab", 14 | "directory": "packages/typeschema" 15 | }, 16 | "version": "1.1.0", 17 | "type": "commonjs", 18 | "files": [ 19 | "src" 20 | ], 21 | "main": "src/index.js", 22 | "dependencies": { 23 | "@typeschema/core": "^0.13.2", 24 | "@typeschema/main": "^0.13.10" 25 | }, 26 | "peerDependencies": { 27 | "@nestjs/common": "^10.0.0 || ^11.0.0", 28 | "@nestjs/core": "^10.0.0 || ^11.0.0", 29 | "reflect-metadata": "^0.1.13 || ^0.2.2" 30 | }, 31 | "peerDependenciesMeta": { 32 | "@nestjs/common": { 33 | "optional": false 34 | }, 35 | "@nestjs/core": { 36 | "optional": false 37 | }, 38 | "reflect-metadata": { 39 | "optional": false 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/typeschema/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeschema", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/typeschema/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "build": { 9 | "executor": "@nx/js:tsc", 10 | "outputs": ["{options.outputPath}"], 11 | "options": { 12 | "outputPath": "dist/packages/typeschema", 13 | "tsConfig": "packages/typeschema/tsconfig.lib.json", 14 | "packageJson": "packages/typeschema/package.json", 15 | "main": "packages/typeschema/src/index.ts", 16 | "assets": ["packages/typeschema/*.md"] 17 | } 18 | }, 19 | "lint": { 20 | "executor": "@nx/eslint:lint", 21 | "outputs": ["{options.outputFile}"] 22 | }, 23 | "test": { 24 | "executor": "@nx/vite:test", 25 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 26 | "options": {} 27 | }, 28 | "publish": { 29 | "executor": "nx:run-commands", 30 | "options": { 31 | "cwd": "dist/packages/typeschema", 32 | "command": "pnpm publish" 33 | }, 34 | "dependsOn": [ 35 | { 36 | "target": "build" 37 | } 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/typeschema/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | -------------------------------------------------------------------------------- /packages/typeschema/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './typeschema.constants'; 2 | export * from './typeschema.dto'; 3 | export * from './typeschema.pipe'; 4 | -------------------------------------------------------------------------------- /packages/typeschema/src/lib/typeschema-options.interface.ts: -------------------------------------------------------------------------------- 1 | import type { ValidationIssue } from '@typeschema/core'; 2 | 3 | export interface ValidationPipeOptions { 4 | exceptionFactory?: (issues: ValidationIssue[]) => Error; 5 | } 6 | -------------------------------------------------------------------------------- /packages/typeschema/src/lib/typeschema.constants.ts: -------------------------------------------------------------------------------- 1 | export const TypeschemaOptions = Symbol('INJECT:VALIDATION_PIPE_OPTIONS'); 2 | -------------------------------------------------------------------------------- /packages/typeschema/src/lib/typeschema.dto.ts: -------------------------------------------------------------------------------- 1 | import { Schema, Infer } from '@typeschema/main'; 2 | 3 | export const TypeschemaDto = ( 4 | schema: TSchema 5 | ): { 6 | new (parsed: Infer): { 7 | data: Infer; 8 | }; 9 | schema: TSchema; 10 | OPENAPI_METADATA: Record; 11 | } => { 12 | class TypeschemaDtoMixin { 13 | static schema = schema; 14 | static _typeschema = true; 15 | static OPENAPI_METADATA = {}; 16 | static _OPENAPI_METADATA_FACTORY() { 17 | return this.OPENAPI_METADATA; 18 | } 19 | data: Infer; 20 | 21 | constructor(parsed: Infer) { 22 | this.data = parsed; 23 | } 24 | } 25 | 26 | return TypeschemaDtoMixin; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/typeschema/src/lib/typeschema.pipe.ts: -------------------------------------------------------------------------------- 1 | import { validate } from '@typeschema/main'; 2 | import type { ValidationIssue } from '@typeschema/core'; 3 | import { 4 | ArgumentMetadata, 5 | BadRequestException, 6 | Inject, 7 | Injectable, 8 | Logger, 9 | Optional, 10 | PipeTransform, 11 | } from '@nestjs/common'; 12 | 13 | import { TypeschemaDto } from './typeschema.dto'; 14 | import { TypeschemaOptions } from './typeschema.constants'; 15 | import { ValidationPipeOptions } from './typeschema-options.interface'; 16 | 17 | @Injectable() 18 | export class ValidationPipe implements PipeTransform { 19 | constructor( 20 | @Optional() 21 | @Inject(TypeschemaOptions) 22 | private readonly options?: ValidationPipeOptions, 23 | @Optional() protected readonly logger?: Logger 24 | ) {} 25 | async transform(value: unknown, metadata: ArgumentMetadata) { 26 | if (!metadata.metatype || !this.isTypeschemaDto(metadata.metatype)) { 27 | return value; 28 | } 29 | const result = await validate(metadata.metatype.schema, value); 30 | if (!result.success) { 31 | this.logger?.error(result.issues, undefined, 'ValidationPipe'); 32 | throw ( 33 | this.options?.exceptionFactory?.(result.issues) ?? 34 | this.exceptionFactory(result.issues) 35 | ); 36 | } 37 | return new metadata.metatype(result.data); 38 | } 39 | 40 | protected isTypeschemaDto( 41 | type: unknown 42 | ): type is ReturnType { 43 | return ( 44 | typeof type === 'function' && 45 | '_typeschema' in type && 46 | type._typeschema === true 47 | ); 48 | } 49 | 50 | protected exceptionFactory(issues: ValidationIssue[]) { 51 | return new BadRequestException(issues); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/typeschema/test/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import { 3 | AjvDto, 4 | ArktypeDto, 5 | IoTsDTO, 6 | JoiDto, 7 | OwDto, 8 | RuntypesDto, 9 | SuperstructDto, 10 | TypeboxDto, 11 | ValibotDto, 12 | YupDto, 13 | ZodDto, 14 | } from './models'; 15 | 16 | @Controller() 17 | export class AppController { 18 | @Post('ajv') 19 | ajvTest(@Body() { data }: AjvDto) { 20 | return data; 21 | } 22 | 23 | @Post('arktype') 24 | arktypeTest(@Body() { data }: ArktypeDto) { 25 | return data; 26 | } 27 | 28 | @Post('io-ts') 29 | ioTsTest(@Body() { data }: IoTsDTO) { 30 | return data; 31 | } 32 | 33 | @Post('joi') 34 | joiTest(@Body() { data }: JoiDto) { 35 | return data; 36 | } 37 | 38 | @Post('ow') 39 | owTest(@Body() { data }: OwDto) { 40 | return data; 41 | } 42 | 43 | @Post('runtypes') 44 | runtypesTest(@Body() { data }: RuntypesDto) { 45 | return data; 46 | } 47 | 48 | @Post('superstruct') 49 | superstructTest(@Body() { data }: SuperstructDto) { 50 | return data; 51 | } 52 | 53 | @Post('typebox') 54 | typeboxTest(@Body() { data }: TypeboxDto) { 55 | return data; 56 | } 57 | 58 | @Post('valibot') 59 | valibotTest(@Body() { data }: ValibotDto) { 60 | return data; 61 | } 62 | 63 | @Post('yup') 64 | yupTest(@Body() { data }: YupDto) { 65 | return data; 66 | } 67 | 68 | @Post('zod') 69 | zodTest(@Body() { data }: ZodDto) { 70 | return data; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/typeschema/test/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { APP_PIPE } from '@nestjs/core'; 3 | import { Test } from '@nestjs/testing'; 4 | import { spec, request } from 'pactum'; 5 | import { afterAll, beforeAll, describe, it } from 'vitest'; 6 | import { AppController } from './app.controller'; 7 | import { ValidationPipe } from '../src'; 8 | 9 | const endpoints = [ 10 | { endpoint: 'ajv' }, 11 | { endpoint: 'arktype' }, 12 | // { endpoint: 'io-ts' }, 13 | { endpoint: 'joi' }, 14 | // { endpoint: 'ow' }, 15 | { endpoint: 'runtypes' }, 16 | { endpoint: 'superstruct' }, 17 | { endpoint: 'typebox' }, 18 | { endpoint: 'valibot' }, 19 | { endpoint: 'yup' }, 20 | { endpoint: 'zod' }, 21 | ] as const; 22 | 23 | describe('TypeschemaPipe integration test', () => { 24 | let app: INestApplication; 25 | 26 | beforeAll(async () => { 27 | const modRef = await Test.createTestingModule({ 28 | controllers: [AppController], 29 | providers: [ 30 | { 31 | provide: APP_PIPE, 32 | useClass: ValidationPipe, 33 | }, 34 | ], 35 | }).compile(); 36 | app = modRef.createNestApplication(); 37 | await app.listen(0); 38 | request.setBaseUrl((await app.getUrl()).replace('[::1]', 'localhost')); 39 | }); 40 | 41 | afterAll(async () => { 42 | await app.close(); 43 | }); 44 | 45 | it.each(endpoints)('call $endpoint - success', async ({ endpoint }) => { 46 | await spec() 47 | .post(`/${endpoint}`) 48 | .withJson({ foo: endpoint, bar: 42 }) 49 | .expectStatus(201) 50 | .expectBody({ foo: endpoint, bar: 42 }) 51 | .toss(); 52 | }); 53 | 54 | it.each(endpoints)('call $endpoint - failure', async ({ endpoint }) => { 55 | await spec() 56 | .post(`/${endpoint}`) 57 | .withJson({ foo: endpoint, bar: true }) 58 | .expectStatus(400) 59 | .expectJsonLike({ 60 | error: 'Bad Request', 61 | statusCode: 400, 62 | }) 63 | .toss(); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/ajv.dto.ts: -------------------------------------------------------------------------------- 1 | import { JSONSchemaType } from 'ajv'; 2 | 3 | import type { CommonInterface } from './common'; 4 | 5 | import { TypeschemaDto } from '../../src'; 6 | 7 | const schema: JSONSchemaType = { 8 | type: 'object', 9 | properties: { 10 | foo: { type: 'string' }, 11 | bar: { type: 'number' }, 12 | }, 13 | required: ['foo', 'bar'], 14 | additionalProperties: false, 15 | }; 16 | 17 | export class AjvDto extends TypeschemaDto(schema) {} 18 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/arktype.dto.ts: -------------------------------------------------------------------------------- 1 | import { type } from 'arktype'; 2 | import { TypeschemaDto } from '../../src'; 3 | 4 | const schema = type({ 5 | foo: 'string', 6 | bar: 'number', 7 | }); 8 | 9 | export class ArktypeDto extends TypeschemaDto(schema) {} 10 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/common.ts: -------------------------------------------------------------------------------- 1 | export interface CommonInterface { 2 | foo: string; 3 | bar: number; 4 | } 5 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ajv.dto'; 2 | export * from './arktype.dto'; 3 | export * from './io-ts.dto'; 4 | export * from './joi.dto'; 5 | export * from './ow.dto'; 6 | export * from './runtypes.dto'; 7 | export * from './superstruct.dto'; 8 | export * from './typebox.dto'; 9 | export * from './valibot.dto'; 10 | export * from './yup.dto'; 11 | export * from './zod.dto'; 12 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/io-ts.dto.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts'; 2 | import { TypeschemaDto } from '../../src'; 3 | 4 | const schema = t.type({ 5 | foo: t.string, 6 | bar: t.number, 7 | }); 8 | 9 | export class IoTsDTO extends TypeschemaDto(schema) {} 10 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/joi.dto.ts: -------------------------------------------------------------------------------- 1 | import Joi from 'joi'; 2 | import { TypeschemaDto } from '../../src'; 3 | 4 | const schema = Joi.object({ 5 | foo: Joi.string(), 6 | bar: Joi.number(), 7 | }); 8 | 9 | export class JoiDto extends TypeschemaDto(schema) {} 10 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/ow.dto.ts: -------------------------------------------------------------------------------- 1 | import ow from 'ow'; 2 | import { TypeschemaDto } from '../../src'; 3 | 4 | const schema = ow.object.exactShape({ 5 | foo: ow.string, 6 | bar: ow.number, 7 | }); 8 | 9 | export class OwDto extends TypeschemaDto(schema) {} 10 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/runtypes.dto.ts: -------------------------------------------------------------------------------- 1 | import { String, Number, Record } from 'runtypes'; 2 | import { TypeschemaDto } from '../../src'; 3 | 4 | const schema = Record({ 5 | foo: String, 6 | bar: Number, 7 | }); 8 | 9 | export class RuntypesDto extends TypeschemaDto(schema) {} 10 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/superstruct.dto.ts: -------------------------------------------------------------------------------- 1 | import { string, number, object } from 'superstruct'; 2 | import { TypeschemaDto } from '../../src'; 3 | 4 | const schema = object({ 5 | foo: string(), 6 | bar: number(), 7 | }); 8 | 9 | export class SuperstructDto extends TypeschemaDto(schema) {} 10 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/typebox.dto.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@sinclair/typebox'; 2 | import { TypeschemaDto } from '../../src'; 3 | 4 | const schema = Type.Object({ 5 | foo: Type.String(), 6 | bar: Type.Number(), 7 | }); 8 | 9 | export class TypeboxDto extends TypeschemaDto(schema) {} 10 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/valibot.dto.ts: -------------------------------------------------------------------------------- 1 | import { object, string, number } from 'valibot'; 2 | import { TypeschemaDto } from '../../src'; 3 | 4 | const schema = object({ 5 | foo: string(), 6 | bar: number(), 7 | }); 8 | 9 | export class ValibotDto extends TypeschemaDto(schema) {} 10 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/yup.dto.ts: -------------------------------------------------------------------------------- 1 | import { object, number, string } from 'yup'; 2 | import { TypeschemaDto } from '../../src'; 3 | 4 | const schema = object({ 5 | foo: string(), 6 | bar: number(), 7 | }); 8 | 9 | export class YupDto extends TypeschemaDto(schema) {} 10 | -------------------------------------------------------------------------------- /packages/typeschema/test/models/zod.dto.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { TypeschemaDto } from '../../src'; 3 | 4 | const schema = z.object({ 5 | foo: z.string(), 6 | bar: z.number(), 7 | }); 8 | 9 | export class ZodDto extends TypeschemaDto(schema) {} 10 | -------------------------------------------------------------------------------- /packages/typeschema/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true 5 | }, 6 | "files": [], 7 | "include": [], 8 | "references": [ 9 | { 10 | "path": "./tsconfig.lib.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /packages/typeschema/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"], 8 | "target": "es2021" 9 | }, 10 | "exclude": ["**/*.spec.ts", "jest.config.ts"], 11 | "include": ["src/**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/typeschema/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [ 6 | "vitest/globals", 7 | "vitest/importMeta", 8 | "vite/client", 9 | "node", 10 | "vitest" 11 | ] 12 | }, 13 | "include": [ 14 | "vite.config.ts", 15 | "vitest.config.ts", 16 | "src/**/*.test.ts", 17 | "src/**/*.spec.ts", 18 | "src/**/*.test.tsx", 19 | "src/**/*.spec.tsx", 20 | "src/**/*.test.js", 21 | "src/**/*.spec.js", 22 | "src/**/*.test.jsx", 23 | "src/**/*.spec.jsx", 24 | "src/**/*.d.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /packages/typeschema/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import swc from 'unplugin-swc'; 3 | import { defineConfig } from 'vite'; 4 | 5 | import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; 6 | 7 | export default defineConfig({ 8 | root: __dirname, 9 | cacheDir: '../../node_modules/.vite/packages/typeschema', 10 | 11 | plugins: [nxViteTsPaths(), swc.vite({ module: { type: 'es6' } })], 12 | 13 | // Uncomment this if you are using workers. 14 | // worker: { 15 | // plugins: [ nxViteTsPaths() ], 16 | // }, 17 | 18 | test: { 19 | watch: false, 20 | globals: true, 21 | environment: 'node', 22 | include: ['test/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 23 | 24 | reporters: ['default'], 25 | coverage: { 26 | reportsDirectory: '../../coverage/packages/typeschema', 27 | provider: 'v8', 28 | }, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/**' 3 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2021", 12 | "module": "CommonJS", 13 | "lib": ["ES2021"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "strict": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@nest-lab/fastify-multer": ["packages/fastify-multer/src/index.ts"], 20 | "@nest-lab/or-guard": ["packages/or-guard/src/index.ts"], 21 | "@nest-lab/throttler-storage-redis": [ 22 | "packages/throttler-storage-redis/src/index.ts" 23 | ], 24 | "@nest-lab/typeschema": ["packages/typeschema/src/index.ts"] 25 | } 26 | }, 27 | "exclude": ["node_modules", "tmp"] 28 | } 29 | -------------------------------------------------------------------------------- /vitest.workspace.ts: -------------------------------------------------------------------------------- 1 | export default ['**/*/vite.config.ts', '**/*/vitest.config.ts']; 2 | --------------------------------------------------------------------------------