├── .github ├── FUNDING.yaml ├── dependabot.yml ├── workflows │ ├── release.yml │ ├── pre-release.yml │ ├── ci.yml │ └── codeql-analysis.yml ├── ISSUE_TEMPLATE │ ├── feature-request---.md │ └── bug-report---.md └── stale.yml ├── lib ├── symbols.js └── dictionary.js ├── .husky └── pre-commit ├── index.d.ts ├── LICENSE ├── package.json ├── .gitignore ├── .npmignore ├── index.js ├── test ├── index.test-d.ts └── index.test.js ├── CHANGELOG.md └── README.md /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: metcoder95 -------------------------------------------------------------------------------- /lib/symbols.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | kSchemas: Symbol.for('#schemas') 3 | } 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run test:ci 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | open-pull-requests-limit: 10 -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | name: Release Creation 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: ncipollo/release-action@v1 16 | with: 17 | bodyFile: "CHANGELOG.md" 18 | token: ${{ secrets.GITHUB_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/pre-release.yml: -------------------------------------------------------------------------------- 1 | name: Pre-Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*.*.*-beta**' 6 | - 'v*.*.*-rc**' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | name: Pre-Release Creation 14 | steps: 15 | - uses: actions/checkout@v6 16 | - uses: ncipollo/release-action@v1 17 | with: 18 | prerelease: true 19 | bodyFile: "CHANGELOG.md" 20 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { FastifyPluginCallback, FastifyContextConfig } from 'fastify'; 3 | import Ajv from 'ajv'; 4 | 5 | interface FastifySplitValidator { 6 | defaultValidator?: Ajv; 7 | } 8 | 9 | declare module 'fastify' { 10 | interface FastifyContextConfig { 11 | schemaValidators: { 12 | body?: Ajv; 13 | headers?: Ajv; 14 | querystring?: Ajv; 15 | query?: Ajv; 16 | params?: Ajv; 17 | }; 18 | } 19 | } 20 | 21 | declare const FastifySplitValidator: FastifyPluginCallback; 22 | 23 | export default FastifySplitValidator; 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Feature request \U0001F916" 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 15 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "help wanted" 11 | - "plugin suggestion" 12 | - "good first issue" 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: false 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Bug report \U0001F41B" 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Carlos Fuentes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/dictionary.js: -------------------------------------------------------------------------------- 1 | const Ajv = require('ajv') 2 | const { kSchemas } = require('./symbols') 3 | 4 | function flatSchemas (schemas) { 5 | const keys = Object.keys(schemas) 6 | 7 | if (keys.length === 0) return [] 8 | 9 | let result = [] 10 | for (const key of keys) { 11 | if (schemas[key] instanceof Ajv) { 12 | result.push(schemas[key]) 13 | } else { 14 | const nestedSchemas = flatSchemas(schemas[key]) 15 | result = result.concat(nestedSchemas) 16 | } 17 | } 18 | 19 | return result 20 | } 21 | 22 | module.exports = class ValidatorDictionary { 23 | constructor () { 24 | this[kSchemas] = {} 25 | } 26 | 27 | addValidator (path, method, httpPart, validator) { 28 | if (this[kSchemas][path] == null) { 29 | this[kSchemas][path] = { 30 | [method]: { 31 | [httpPart]: validator 32 | } 33 | } 34 | } else if (this[kSchemas][path][method] == null) { 35 | this[kSchemas][path][method] = { 36 | [httpPart]: validator 37 | } 38 | } else { 39 | this[kSchemas][path][method][httpPart] = validator 40 | } 41 | } 42 | 43 | getValidator (path, method, httpPart) { 44 | return this[kSchemas][path]?.[method]?.[httpPart] 45 | } 46 | 47 | getValidators () { 48 | return flatSchemas(this[kSchemas]) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-split-validator", 3 | "version": "5.0.0", 4 | "description": "Validate each HTTP body message part and Queryparams with different AJV schemas", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "test": "tap --cov test/*.test.js && npm run typescript", 9 | "test:ci": "tap --cov test/*.test.js && npm run typescript && npm run lint", 10 | "test:only": "tap --only", 11 | "test:unit": "tap test/*.test.js", 12 | "lint": "standard | snazzy", 13 | "lint:ci": "standard", 14 | "typescript": "tsd", 15 | "release": "npx standard-version" 16 | }, 17 | "engines": { 18 | "node": ">=18.x" 19 | }, 20 | "keywords": [ 21 | "fastify", 22 | "ajv", 23 | "validator", 24 | "fastify-split-validator" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/MetCoder95/fastify-split-validator.git" 29 | }, 30 | "readme": "https://github.com/MetCoder95/fastify-split-validator/blob/main/README.md", 31 | "bugs": { 32 | "url": "https://github.com/MetCoder95/fastify-split-validator/issues" 33 | }, 34 | "author": "MetCoder95 ", 35 | "license": "MIT", 36 | "devDependencies": { 37 | "@types/node": "^24.0.14", 38 | "fastify": "^5.2.1", 39 | "husky": "^9.0.11", 40 | "proxyquire": "^2.1.3", 41 | "snazzy": "^9.0.0", 42 | "standard": "^17.0.0", 43 | "tap": "^16.3.0", 44 | "tsd": "^0.33.0", 45 | "typescript": "^5.0" 46 | }, 47 | "dependencies": { 48 | "ajv": "^8.11.0", 49 | "fastify-plugin": "^5.0.1" 50 | }, 51 | "tsd": { 52 | "directory": "test" 53 | }, 54 | "tap": { 55 | "check-coverage": false 56 | }, 57 | "standard": { 58 | "ignore": [ 59 | "*.d.ts", 60 | "*.test-d.ts" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # package-lock.json 107 | package-lock.json -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | - next 6 | - 'v*' 7 | pull_request: 8 | paths-ignore: 9 | - LICENSE 10 | - '*.md' 11 | 12 | name: CI 13 | 14 | jobs: 15 | lint: 16 | permissions: 17 | contents: read 18 | name: Lint 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 22 | - name: Install Node.js 23 | uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 24 | with: 25 | node-version: v22.x 26 | cache: 'npm' 27 | cache-dependency-path: package.json 28 | 29 | - name: Install dependencies 30 | run: npm install 31 | - name: Check linting 32 | run: npm run lint:ci 33 | 34 | tests: 35 | permissions: 36 | contents: read 37 | name: Tests 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | os: [ubuntu-latest, macos-latest, windows-latest] 42 | node-version: [20.x, 22.x, 24.x] 43 | runs-on: ${{matrix.os}} 44 | steps: 45 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 46 | with: 47 | persist-credentials: false 48 | 49 | - name: Use Node.js ${{ matrix.node-version }} 50 | uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 51 | with: 52 | node-version: ${{ matrix.node-version }} 53 | cache: 'npm' 54 | cache-dependency-path: package.json 55 | 56 | - name: Install Dependencies 57 | run: npm install 58 | 59 | - name: Run Tests 60 | run: npm run test:ci 61 | 62 | automerge: 63 | if: > 64 | github.event_name == 'pull_request' && github.event.pull_request.user.login == 'dependabot[bot]' 65 | needs: 66 | - tests 67 | runs-on: ubuntu-latest 68 | permissions: 69 | contents: write 70 | pull-requests: write 71 | steps: 72 | - name: Merge Dependabot PR 73 | uses: fastify/github-action-merge-dependabot@1b2ed42db8f9d81a46bac83adedfc03eb5149dff # v3.11.2 74 | with: 75 | github-token: ${{ secrets.GITHUB_TOKEN }} 76 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | # As is a package, the lockfile is not needed 108 | package-lock.json 109 | .github 110 | .husky 111 | tsconfig.json -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '30 12 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Code Q Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript', 'typescript' ] 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v6 40 | 41 | # Initializes the CodeQL tools for scanning. 42 | - name: Initialize CodeQL 43 | uses: github/codeql-action/init@v4 44 | with: 45 | languages: ${{ matrix.language }} 46 | # If you wish to specify custom queries, you can do so here or in a config file. 47 | # By default, queries listed here will override any specified in a config file. 48 | # Prefix the list here with "+" to use these queries and those in the config file. 49 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 50 | 51 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 52 | # If this step fails, then you should remove it and run the build manually (see below) 53 | - name: Autobuild 54 | uses: github/codeql-action/autobuild@v4 55 | 56 | # ℹ️ Command-line programs to run using the OS shell. 57 | # 📚 https://git.io/JvXDl 58 | 59 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 60 | # and modify them (or add more) to build your code if your project 61 | # uses a compiled language 62 | 63 | #- run: | 64 | # make bootstrap 65 | # make release 66 | 67 | - name: Perform CodeQL Analysis 68 | uses: github/codeql-action/analyze@v4 69 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fp = require('fastify-plugin') 4 | const Ajv = require('ajv') 5 | const ValidatorDictionary = require('./lib/dictionary') 6 | function plugin (fastifyInstance, opts = {}, done) { 7 | let { defaultValidator } = opts 8 | 9 | // validation and more 10 | const dictionary = new ValidatorDictionary() 11 | fastifyInstance.addHook('onRoute', params => { 12 | if (params.config?.schemaValidators != null) { 13 | const { path, method, config } = params 14 | const compilers = config.schemaValidators 15 | const keys = Object.keys(compilers) 16 | 17 | for (const key of keys) { 18 | dictionary.addValidator( 19 | path, 20 | method, 21 | // If query passed, we should change it to querystring 22 | key === 'query' ? 'querystring' : key, 23 | compilers[key] 24 | ) 25 | } 26 | } 27 | }) 28 | 29 | fastifyInstance.setSchemaController({ 30 | compilersFactory: { 31 | // TODO: Maybe the same for serializer? 32 | buildValidator: function (externalSchemas, ajvServerOptions) { 33 | // We load schemas if any 34 | const schemaIds = 35 | externalSchemas != null ? Object.keys(externalSchemas) : [] 36 | defaultValidator = defaultValidator ?? new Ajv(ajvServerOptions) 37 | 38 | if (schemaIds.length > 0) { 39 | const validators = dictionary.getValidators() 40 | 41 | for (const schemaKey of schemaIds) { 42 | const schema = externalSchemas[schemaKey] 43 | for (const validator of validators) { 44 | // Check if schema added or not for custom validators 45 | if (validator.getSchema(schemaKey) == null) { 46 | validator.addSchema(schema, schemaKey) 47 | } 48 | } 49 | 50 | // Also add it to default validator as fallback 51 | if (defaultValidator.getSchema(schemaKey) == null) { 52 | defaultValidator.addSchema(schema, schemaKey) 53 | } 54 | } 55 | } 56 | 57 | return function validatorCompiler ({ schema, method, url, httpPart }) { 58 | const httpPartValidator = dictionary.getValidator( 59 | url, 60 | method, 61 | httpPart 62 | ) 63 | // We compile for cache all schemas for performance 64 | const fallback = defaultValidator.compile(schema) 65 | 66 | if (httpPartValidator == null) { 67 | return fallback 68 | } 69 | 70 | return httpPartValidator.compile(schema) 71 | } 72 | } 73 | } 74 | }) 75 | 76 | done() 77 | } 78 | 79 | module.exports = fp(plugin, { 80 | fastify: '>=4', 81 | name: 'fastify-split-validator' 82 | }) 83 | -------------------------------------------------------------------------------- /test/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import Ajv from 'ajv' 3 | import plugin from '..' 4 | 5 | const serverHttp = fastify() 6 | const customAjv = new Ajv() 7 | 8 | serverHttp.register(plugin) 9 | 10 | serverHttp.addSchema({ 11 | $id: 'hello', 12 | type: 'string' 13 | }) 14 | 15 | serverHttp.register(plugin, { 16 | defaultValidator: customAjv 17 | }) 18 | 19 | serverHttp.get('/', { 20 | schema: { 21 | querystring: { 22 | hello: { 23 | $ref: 'hello#' 24 | } 25 | } 26 | }, 27 | config: { 28 | schemaValidators: { 29 | querystring: customAjv 30 | } 31 | } 32 | } , async (request, reply) => {}) 33 | 34 | 35 | // -> Second level 36 | serverHttp.register( 37 | function (fastifyInstance, opts, done) { 38 | fastifyInstance.register(plugin) 39 | 40 | fastifyInstance.get('/string',{ 41 | schema:{ 42 | params: { 43 | hello: { 44 | $ref: 'hello#', 45 | } 46 | }, 47 | response:{ 48 | 200:{ 49 | type:'string' 50 | } 51 | }, 52 | }, 53 | config: { 54 | schemaValidators: { 55 | params: customAjv 56 | } 57 | } 58 | }, (req, reply) => { 59 | reply.send({ 60 | hello: 'world' 61 | }) 62 | }) 63 | 64 | // Sending a JSON 65 | serverHttp.post('/json', (req, reply) => { 66 | reply.send({ foo: 'bar' }) 67 | }) 68 | 69 | 70 | done() 71 | }, 72 | { prefix: '/api' } 73 | ) 74 | 75 | const serverHttp2 = fastify({ http2: true }) 76 | serverHttp2.addSchema({ 77 | $id: 'hello', 78 | type: 'string' 79 | }) 80 | 81 | 82 | serverHttp2.register(plugin, { 83 | defaultValidator: new Ajv() 84 | }) 85 | 86 | serverHttp2.get('/', { 87 | schema: { 88 | querystring: { 89 | hello: { 90 | $ref: 'hello#' 91 | } 92 | } 93 | }, 94 | config: { 95 | schemaValidators: { 96 | querystring: customAjv 97 | } 98 | } 99 | } , async (request, reply) => {}) 100 | 101 | // -> First plugin 102 | serverHttp2.register( 103 | function (fastifyInstance, opts, done) { 104 | fastifyInstance.get('/string',{ 105 | schema:{ 106 | params: { 107 | hello: { 108 | $ref: 'hello#', 109 | } 110 | }, 111 | response:{ 112 | 200:{ 113 | type:'string' 114 | } 115 | }, 116 | }, 117 | config: { 118 | schemaValidators: { 119 | params: customAjv 120 | } 121 | } 122 | }, (req, reply) => { 123 | reply.send({ 124 | hello: 'world' 125 | }) 126 | }) 127 | 128 | // Sending a JSON 129 | fastifyInstance.post('/json', (req, reply) => { 130 | reply.send({ foo: 'bar' }) 131 | }) 132 | 133 | done() 134 | }, 135 | { prefix: '/api' } 136 | ) 137 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [5.0.0](https://github.com/MetCoder95/fastify-split-validator/compare/v4.0.0...v5.0.0) (2025-07-17) 6 | 7 | 8 | ### ⚠ BREAKING CHANGES 9 | 10 | * drop support for node v18 11 | 12 | ### Features 13 | 14 | * drop support for node v18 ([049be6c](https://github.com/MetCoder95/fastify-split-validator/commit/049be6c30b8fc3938bba2eebe3e1b0be2750e725)) 15 | 16 | ## [4.0.0](https://github.com/MetCoder95/fastify-split-validator/compare/v3.0.0...v4.0.0) (2023-12-25) 17 | 18 | 19 | ### ⚠ BREAKING CHANGES 20 | 21 | * drop support for older Node 22 | 23 | ### Features 24 | 25 | * drop support for older Node ([f8aaa65](https://github.com/MetCoder95/fastify-split-validator/commit/f8aaa658bcd9a4de6a2cae24c9e9ac8008c9a0a8)) 26 | 27 | ## [3.0.0](https://github.com/MetCoder95/fastify-split-validator/compare/v2.0.0...v3.0.0) (2023-05-18) 28 | 29 | 30 | ### ⚠ BREAKING CHANGES 31 | 32 | * drop support for Node v14 33 | 34 | ### Features 35 | 36 | * drop support for Node v14 ([1dbd938](https://github.com/MetCoder95/fastify-split-validator/commit/1dbd938b39e0e6bd53e87033fd235b81c8cab925)) 37 | 38 | ## [2.0.0](https://github.com/MetCoder95/fastify-split-validator/compare/v1.0.2...v2.0.0) (2022-10-16) 39 | 40 | 41 | ### ⚠ BREAKING CHANGES 42 | 43 | * v2 (#3) 44 | 45 | ### Features 46 | 47 | * v2 ([#3](https://github.com/MetCoder95/fastify-split-validator/issues/3)) ([10bf649](https://github.com/MetCoder95/fastify-split-validator/commit/10bf64987730eca37de6d0eb8ff50d6eae9335ec)) 48 | 49 | 50 | ### Bug Fixes 51 | 52 | * fix for automatic HEAD routes ([#2](https://github.com/MetCoder95/fastify-split-validator/issues/2)) ([eb14b76](https://github.com/MetCoder95/fastify-split-validator/commit/eb14b76d766f17ea955251b858c2ab2aa35a977b)) 53 | 54 | ### [1.0.2](https://github.com/MetCoder95/fastify-split-validator/compare/v1.0.1...v1.0.2) (2021-12-21) 55 | 56 | 57 | ### Bug Fixes 58 | 59 | * package metadata ([1dbcc27](https://github.com/MetCoder95/fastify-split-validator/commit/1dbcc2728c46365ed1ff0e9bdfd461331b00ec13)) 60 | 61 | ### [1.0.1](https://github.com/MetCoder94/fastify-split-validator/compare/v1.0.0...v1.0.1) (2021-12-17) 62 | 63 | ## 1.0.0 (2021-12-15) 64 | 65 | 66 | ### ⚠ BREAKING CHANGES 67 | 68 | * v1 (#1) 69 | 70 | ### Features 71 | 72 | * v1 ([#1](https://github.com/MetCoder95/fastify-split-validator/issues/1)) ([570e631](https://github.com/MetCoder95/fastify-split-validator/commit/570e6317c6a0f1046d049c343988b700e0594571)) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * name ([214945e](https://github.com/MetCoder95/fastify-split-validator/commit/214945ea16e77faecb03ee99426231211a71c038)) 78 | * remote leftover ([89adbf9](https://github.com/MetCoder95/fastify-split-validator/commit/89adbf973c3989e91d158f6aec8331dd79958f33)) 79 | 80 | ## 1.0.0-rc.0 (2021-12-14) 81 | 82 | 83 | ### ⚠ BREAKING CHANGES 84 | 85 | * v1 (#1) 86 | 87 | ### Features 88 | 89 | * v1 ([#1](https://github.com/MetCoder95/fastify-split-validator/issues/1)) ([570e631](https://github.com/MetCoder95/fastify-split-validator/commit/570e6317c6a0f1046d049c343988b700e0594571)) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * name ([214945e](https://github.com/MetCoder95/fastify-split-validator/commit/214945ea16e77faecb03ee99426231211a71c038)) 95 | * remote leftover ([89adbf9](https://github.com/MetCoder95/fastify-split-validator/commit/89adbf973c3989e91d158f6aec8331dd79958f33)) 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastify-split-validator 2 | 3 | [![CI](https://github.com/MetCoder95/fastify-split-validator/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/MetCoder95/fastify-split-validator/actions/workflows/ci.yml) [![CodeQL](https://github.com/MetCoder95/fastify-split-validator/actions/workflows/codeql-analysis.yml/badge.svg?branch=main)](https://github.com/MetCoder95/fastify-split-validator/actions/workflows/codeql-analysis.yml) ![npm](https://img.shields.io/npm/v/fastify-split-validator) 4 | 5 | --- 6 | 7 | `fastify-split-validator` is a plugin which allows you to setup, granularly, different validates per HTTP part of the request. This works at a route level, doing a fallback into a default validator using the default server config from the instance where the plugin is being installed. 8 | 9 | You can provide your own default validator to act as fallback in case this is not defined within the definition of the route (_by default uses Ajv@8 as default fallback_). 10 | 11 | ## Setup 12 | 13 | Install by running `npm install fastify-split-validator`. 14 | 15 | ### Options 16 | 17 | **Instance** 18 | 19 | - `defaultValidator`: default validator to be used as fallback in case nothing is provided at a route level definition 20 | 21 | Example: 22 | 23 | ```js 24 | const fastify = require('fastify'); 25 | const splitValidator = require('fastify-split-validator'); 26 | const Ajv = require('ajv'); 27 | 28 | const app = fastify(); 29 | const validator = new Ajv({}); 30 | 31 | await app.register(splitValidator, { defaultValidator: validator }); 32 | ``` 33 | 34 | >**Note**: 35 | > It is important to advice that with the new fastify@v4, all the route registration now happens asynchronously. 36 | > This change translates in a way that if any plugin is meant to set logic into the `onRoute` hook for manipulating 37 | > routes after registration, it is necessary to await until the plugin is fully loaded before proceeding with the next parts 38 | > of your route definition. Otherwise, this can lead to non-deterministic behaviours when the plugin will not the expected 39 | > effect on your fastify application. 40 | 41 | **On Route** 42 | 43 | - `schemaValidators`: an object with the HTTP parts as keys and the validators to be used for that part as values 44 | - `schemaValidators.body`: validator to be used for the body of the request 45 | - `schemaValidators.params`: validator to be used for the params of the request 46 | - `schemaValidators.headers`: validator to be used for the headers of the request 47 | - `schemaValidators.querystring`: validator to be used for the querystring of the request 48 | - `schemaValidators.query`: alias for `schemaValidators.querystring` 49 | 50 | ### TypeScript 51 | 52 | ```ts 53 | import fastify from 'fastify'; 54 | import splitValidator from 'fastify-split-validator'; 55 | import Ajv from 'ajv'; 56 | 57 | const app = fastify(); 58 | const validator = new Ajv({}); 59 | const bodyValidator = new Ajv({}); 60 | const headersValidator = new Ajv({}); 61 | 62 | await app.register(splitValidator, { defaultValidator: validator }); 63 | 64 | app.post('/', { 65 | config: { 66 | schemaValidators: { 67 | body: bodyValidator, 68 | headers: headersValidator, 69 | }, 70 | }, 71 | }, (req, reply) => { 72 | // ... 73 | }); 74 | ``` 75 | 76 | ### JavaScript 77 | ```js 78 | const fastify = require('fastify'); 79 | const splitValidator = require('fastify-split-validator'); 80 | const Ajv = require('ajv'); 81 | 82 | const app = fastify(); 83 | const validator = new Ajv({}); 84 | const bodyValidator = new Ajv({}); 85 | const headersValidator = new Ajv({}); 86 | 87 | await app.register(splitValidator, { defaultValidator: validator }); 88 | 89 | app.post('/', { 90 | config: { 91 | schemaValidators: { 92 | body: bodyValidator, 93 | headers: headersValidator, 94 | }, 95 | }, 96 | }, (req, reply) => { 97 | // ... 98 | }); 99 | ``` 100 | 101 | See [test](test/index.test.js) for more examples. -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const tap = require('tap') 4 | const AJV = require('ajv') 5 | const Fastify = require('fastify') 6 | const proxyquire = require('proxyquire') 7 | const plugin = require('..') 8 | 9 | const test = tap.test 10 | 11 | tap.plan(15) 12 | 13 | test( 14 | 'Should allow custom AJV instance for querystring', 15 | { only: true }, 16 | async t => { 17 | t.plan(1) 18 | const customAjv = new AJV({ coerceTypes: false }) 19 | const server = Fastify() 20 | 21 | server.register(plugin, {}) 22 | 23 | server.get( 24 | '/', 25 | { 26 | schema: { 27 | querystring: { 28 | type: 'object', 29 | properties: { 30 | msg: { 31 | type: 'array', 32 | items: { 33 | type: 'string' 34 | } 35 | } 36 | } 37 | } 38 | }, 39 | config: { 40 | schemaValidators: { 41 | // TODO: normalize to query 42 | querystring: customAjv 43 | } 44 | } 45 | }, 46 | (req, reply) => {} 47 | ) 48 | 49 | try { 50 | const res = await server.inject({ 51 | method: 'GET', 52 | url: '/', 53 | query: { 54 | msg: ['hello world'] 55 | } 56 | }) 57 | 58 | t.equal( 59 | res.statusCode, 60 | 400, 61 | 'Should coerce the single element array into string' 62 | ) 63 | } catch (err) { 64 | t.error(err) 65 | } 66 | } 67 | ) 68 | 69 | test('Should allow custom AJV instance for body', async t => { 70 | t.plan(2) 71 | const customAjv = new AJV({ coerceTypes: false }) 72 | const server = Fastify() 73 | 74 | server.register(plugin, {}) 75 | 76 | server.post( 77 | '/', 78 | { 79 | schema: { 80 | body: { 81 | type: 'object', 82 | properties: { 83 | msg: { 84 | type: 'array', 85 | items: { 86 | type: 'string' 87 | } 88 | } 89 | } 90 | } 91 | }, 92 | config: { 93 | schemaValidators: { 94 | body: customAjv 95 | } 96 | } 97 | }, 98 | (req, reply) => {} 99 | ) 100 | 101 | try { 102 | const res = await server.inject({ 103 | method: 'POST', 104 | url: '/', 105 | payload: { 106 | msg: 'hello world' 107 | } 108 | }) 109 | 110 | const body = res.json() 111 | 112 | t.equal(body.message, 'body/msg must be array') 113 | t.equal( 114 | res.statusCode, 115 | 400, 116 | 'Should coerce the single element array into string' 117 | ) 118 | } catch (err) { 119 | t.error(err) 120 | } 121 | }) 122 | 123 | test('Should allow custom AJV instance for params', async t => { 124 | t.plan(2) 125 | const customAjv = new AJV({ coerceTypes: false }) 126 | const server = Fastify() 127 | 128 | server.register(plugin, {}) 129 | 130 | server.get( 131 | '/:msg', 132 | { 133 | schema: { 134 | params: { 135 | type: 'object', 136 | properties: { 137 | msg: { 138 | type: 'integer' 139 | } 140 | } 141 | } 142 | }, 143 | config: { 144 | schemaValidators: { 145 | params: customAjv 146 | } 147 | } 148 | }, 149 | (req, reply) => {} 150 | ) 151 | 152 | try { 153 | const res = await server.inject({ 154 | method: 'GET', 155 | url: '/1' 156 | }) 157 | 158 | const body = res.json() 159 | 160 | t.equal(body.message, 'params/msg must be integer') 161 | t.equal( 162 | res.statusCode, 163 | 400, 164 | 'Should coerce the single element array into string' 165 | ) 166 | } catch (err) { 167 | t.error(err) 168 | } 169 | }) 170 | 171 | test('Should allow custom AJV instance for headers', async t => { 172 | t.plan(2) 173 | const customAjv = new AJV({ coerceTypes: false }) 174 | const server = Fastify() 175 | 176 | server.register(plugin, {}) 177 | 178 | server.get( 179 | '/', 180 | { 181 | schema: { 182 | headers: { 183 | type: 'object', 184 | properties: { 185 | 'x-type': { 186 | type: 'integer' 187 | } 188 | } 189 | } 190 | }, 191 | config: { 192 | schemaValidators: { 193 | headers: customAjv 194 | } 195 | } 196 | }, 197 | (req, reply) => {} 198 | ) 199 | 200 | try { 201 | const res = await server.inject({ 202 | method: 'GET', 203 | path: '/', 204 | headers: { 205 | 'x-type': '1' 206 | } 207 | }) 208 | 209 | const body = res.json() 210 | 211 | // TODO: set into documentation that it's possible the 212 | // error formatter doesn't work as expected. 213 | // Custom one should be provided 214 | t.equal(body.message, 'headers/x-type must be integer') 215 | t.equal( 216 | res.statusCode, 217 | 400, 218 | 'Should coerce the single element array into string' 219 | ) 220 | } catch (err) { 221 | t.error(err) 222 | } 223 | }) 224 | 225 | test('Should work with referenced schemas (querystring)', async t => { 226 | t.plan(2) 227 | const customAjv = new AJV({ coerceTypes: false }) 228 | const server = Fastify() 229 | 230 | server.addSchema({ 231 | $id: 'some', 232 | type: 'array', 233 | items: { 234 | type: 'string' 235 | } 236 | }) 237 | 238 | server.register(plugin, {}) 239 | 240 | // The issue is at the `Fastify#setSchemaControler` level, 241 | // as when adding a new SchemaController the parent is passed 242 | // instead of the same old Schema Controller, causing 243 | // to lose the reference to the prior registered Schemas. 244 | // Reported at: https://github.com/fastify/fastify/issues/3121 245 | server.get( 246 | '/', 247 | { 248 | schema: { 249 | query: { 250 | type: 'object', 251 | properties: { 252 | msg: { 253 | $ref: 'some#' 254 | } 255 | } 256 | } 257 | }, 258 | config: { 259 | schemaValidators: { 260 | querystring: customAjv 261 | } 262 | } 263 | }, 264 | (req, reply) => { 265 | reply.send({ noop: 'noop' }) 266 | } 267 | ) 268 | 269 | try { 270 | const res = await server.inject({ 271 | method: 'GET', 272 | url: '/', 273 | query: { 274 | msg: ['hello world'] 275 | } 276 | }) 277 | 278 | const body = res.json() 279 | 280 | t.equal(body.message, 'querystring/msg must be array') 281 | t.equal( 282 | res.statusCode, 283 | 400, 284 | 'Should parse the single element array into string' 285 | ) 286 | } catch (err) { 287 | t.error(err) 288 | } 289 | }) 290 | 291 | test('Should work with referenced schemas (params)', async t => { 292 | t.plan(2) 293 | const customAjv = new AJV({ coerceTypes: false }) 294 | const server = Fastify() 295 | 296 | server.addSchema({ 297 | $id: 'some', 298 | type: 'integer' 299 | }) 300 | 301 | server.register(plugin, {}) 302 | 303 | server.get( 304 | '/:id', 305 | { 306 | schema: { 307 | params: { 308 | type: 'object', 309 | properties: { 310 | id: { 311 | $ref: 'some#' 312 | } 313 | } 314 | } 315 | }, 316 | config: { 317 | schemaValidators: { 318 | params: customAjv 319 | } 320 | } 321 | }, 322 | (req, reply) => { 323 | reply.send({ noop: 'noop' }) 324 | } 325 | ) 326 | 327 | try { 328 | const res = await server.inject({ 329 | method: 'GET', 330 | url: '/1' 331 | }) 332 | 333 | const body = res.json() 334 | 335 | t.equal(body.message, 'params/id must be integer') 336 | t.equal(res.statusCode, 400, 'Should not coearce the string into integer') 337 | } catch (err) { 338 | t.error(err) 339 | } 340 | }) 341 | 342 | test('Should work with referenced schemas (headers)', async t => { 343 | t.plan(2) 344 | const customAjv = new AJV({ coerceTypes: false }) 345 | const server = Fastify() 346 | 347 | server.addSchema({ 348 | $id: 'some', 349 | type: 'integer' 350 | }) 351 | 352 | server.register(plugin, {}) 353 | 354 | server.get( 355 | '/', 356 | { 357 | schema: { 358 | headers: { 359 | type: 'object', 360 | properties: { 361 | 'x-id': { 362 | $ref: 'some#' 363 | } 364 | } 365 | } 366 | }, 367 | config: { 368 | schemaValidators: { 369 | headers: customAjv 370 | } 371 | } 372 | }, 373 | (req, reply) => { 374 | reply.send({ noop: 'noop' }) 375 | } 376 | ) 377 | 378 | try { 379 | const res = await server.inject({ 380 | method: 'GET', 381 | url: '/', 382 | headers: { 383 | 'x-id': '1' 384 | } 385 | }) 386 | 387 | const body = res.json() 388 | 389 | t.equal(body.message, 'headers/x-id must be integer') 390 | t.equal(res.statusCode, 400, 'Should not coearce the string into integer') 391 | } catch (err) { 392 | t.error(err) 393 | } 394 | }) 395 | test('Should work with referenced schemas (body)', async t => { 396 | t.plan(2) 397 | const customAjv = new AJV({ coerceTypes: false }) 398 | const server = Fastify() 399 | 400 | server.addSchema({ 401 | $id: 'some', 402 | type: 'string' 403 | }) 404 | 405 | server.register(plugin, {}) 406 | 407 | server.post( 408 | '/', 409 | { 410 | schema: { 411 | body: { 412 | type: 'object', 413 | properties: { 414 | msg: { 415 | $ref: 'some#' 416 | } 417 | } 418 | } 419 | }, 420 | config: { 421 | schemaValidators: { 422 | body: customAjv 423 | } 424 | } 425 | }, 426 | (req, reply) => { 427 | reply.send({ noop: 'noop' }) 428 | } 429 | ) 430 | 431 | try { 432 | const res = await server.inject({ 433 | method: 'POST', 434 | url: '/', 435 | payload: { 436 | msg: 1 437 | } 438 | }) 439 | 440 | const body = res.json() 441 | 442 | t.equal(body.message, 'body/msg must be string') 443 | t.equal(res.statusCode, 400, 'Should not coearce the string into integer') 444 | } catch (err) { 445 | t.error(err) 446 | } 447 | }) 448 | 449 | test('Should work with parent and same instance schemas', async t => { 450 | t.plan(2) 451 | const customAjv = new AJV({ coerceTypes: false }) 452 | const server = Fastify() 453 | 454 | server.addSchema({ 455 | $id: 'some', 456 | type: 'string' 457 | }) 458 | 459 | server.register((instance, opts, done) => { 460 | instance.addSchema({ 461 | $id: 'another', 462 | type: 'string' 463 | }) 464 | 465 | // #TODO: Another bug, schemas defined within the same 466 | // encaptulated plugin are not being registered 467 | instance.register(plugin, {}) 468 | 469 | instance.post( 470 | '/', 471 | { 472 | schema: { 473 | body: { 474 | type: 'object', 475 | properties: { 476 | msg: { 477 | $ref: 'some#' 478 | }, 479 | another: { 480 | $ref: 'another#' 481 | } 482 | } 483 | } 484 | }, 485 | config: { 486 | schemaValidators: { 487 | body: customAjv 488 | } 489 | } 490 | }, 491 | (req, reply) => { 492 | reply.send({ noop: 'noop' }) 493 | } 494 | ) 495 | 496 | done() 497 | }) 498 | 499 | try { 500 | const res = await server.inject({ 501 | method: 'POST', 502 | url: '/', 503 | payload: { 504 | msg: 1 505 | } 506 | }) 507 | 508 | const body = res.json() 509 | 510 | t.equal(body.message, 'body/msg must be string') 511 | t.equal(res.statusCode, 400, 'Should not coearce the string into integer') 512 | } catch (err) { 513 | t.error(err) 514 | } 515 | }) 516 | 517 | test('Should work with parent schemas', async t => { 518 | t.plan(2) 519 | const customAjv = new AJV({ coerceTypes: false }) 520 | const server = Fastify() 521 | 522 | server.addSchema({ 523 | $id: 'some', 524 | type: 'string' 525 | }) 526 | 527 | server.register((instance, opts, done) => { 528 | instance.register(plugin, {}) 529 | 530 | instance.post( 531 | '/', 532 | { 533 | schema: { 534 | body: { 535 | type: 'object', 536 | properties: { 537 | msg: { 538 | $ref: 'some#' 539 | } 540 | } 541 | } 542 | }, 543 | config: { 544 | schemaValidators: { 545 | body: customAjv 546 | } 547 | } 548 | }, 549 | (req, reply) => { 550 | reply.send({ noop: 'noop' }) 551 | } 552 | ) 553 | 554 | done() 555 | }) 556 | 557 | try { 558 | const res = await server.inject({ 559 | method: 'POST', 560 | url: '/', 561 | payload: { 562 | msg: 1 563 | } 564 | }) 565 | 566 | const body = res.json() 567 | 568 | t.equal(body.message, 'body/msg must be string') 569 | t.equal(res.statusCode, 400, 'Should not coearce the string into integer') 570 | } catch (err) { 571 | t.error(err) 572 | } 573 | }) 574 | 575 | test('Should work with parent nested schemas', async t => { 576 | t.plan(4) 577 | const customAjv = new AJV({ coerceTypes: false }) 578 | const server = Fastify() 579 | 580 | server.addSchema({ 581 | $id: 'some', 582 | type: 'array', 583 | items: { 584 | type: 'string' 585 | } 586 | }) 587 | 588 | server.register((instance, opts, done) => { 589 | instance.addSchema({ 590 | $id: 'another', 591 | type: 'integer' 592 | }) 593 | 594 | instance.register((subInstance, opts, innerDone) => { 595 | subInstance.register(plugin, {}) 596 | 597 | subInstance.post( 598 | '/', 599 | { 600 | schema: { 601 | querystring: { 602 | type: 'object', 603 | properties: { 604 | msg: { 605 | $ref: 'some#' 606 | } 607 | } 608 | }, 609 | headers: { 610 | type: 'object', 611 | properties: { 612 | 'x-another': { 613 | $ref: 'another#' 614 | } 615 | } 616 | } 617 | }, 618 | config: { 619 | schemaValidators: { 620 | querystring: customAjv, 621 | headers: customAjv 622 | } 623 | } 624 | }, 625 | (req, reply) => { 626 | reply.send({ noop: 'noop' }) 627 | } 628 | ) 629 | 630 | innerDone() 631 | }) 632 | 633 | done() 634 | }) 635 | 636 | try { 637 | const [res1, res2] = await Promise.all([ 638 | server.inject({ 639 | method: 'POST', 640 | url: '/', 641 | query: { 642 | msg: ['string'] 643 | } 644 | }), 645 | server.inject({ 646 | method: 'POST', 647 | url: '/', 648 | headers: { 649 | 'x-another': '1' 650 | } 651 | }) 652 | ]) 653 | 654 | t.equal(res1.json().message, 'querystring/msg must be array') 655 | t.equal(res1.statusCode, 400, 'Should not coearce the string into array') 656 | t.equal(res2.json().message, 'headers/x-another must be integer') 657 | t.equal(res2.statusCode, 400, 'Should not coearce the string into integer') 658 | } catch (err) { 659 | t.error(err) 660 | } 661 | }) 662 | 663 | test('Should handle parsing to querystring (query)', async t => { 664 | t.plan(4) 665 | const customAjv = new AJV({ coerceTypes: false }) 666 | const server = Fastify() 667 | 668 | server.addSchema({ 669 | $id: 'some', 670 | type: 'array', 671 | items: { 672 | type: 'string' 673 | } 674 | }) 675 | 676 | server.register((instance, opts, done) => { 677 | instance.addSchema({ 678 | $id: 'another', 679 | type: 'integer' 680 | }) 681 | 682 | instance.register((subInstance, opts, innerDone) => { 683 | subInstance.register(plugin, {}) 684 | 685 | subInstance.post( 686 | '/', 687 | { 688 | schema: { 689 | query: { 690 | type: 'object', 691 | properties: { 692 | msg: { 693 | $ref: 'some#' 694 | } 695 | } 696 | }, 697 | headers: { 698 | type: 'object', 699 | properties: { 700 | 'x-another': { 701 | $ref: 'another#' 702 | } 703 | } 704 | } 705 | }, 706 | config: { 707 | schemaValidators: { 708 | query: customAjv, 709 | headers: customAjv 710 | } 711 | } 712 | }, 713 | (req, reply) => { 714 | reply.send({ noop: 'noop' }) 715 | } 716 | ) 717 | 718 | innerDone() 719 | }) 720 | 721 | done() 722 | }) 723 | 724 | try { 725 | const [res1, res2] = await Promise.all([ 726 | server.inject({ 727 | method: 'POST', 728 | url: '/', 729 | query: { 730 | msg: ['string'] 731 | } 732 | }), 733 | server.inject({ 734 | method: 'POST', 735 | url: '/', 736 | headers: { 737 | 'x-another': '1' 738 | } 739 | }) 740 | ]) 741 | 742 | t.equal(res1.json().message, 'querystring/msg must be array') 743 | t.equal(res1.statusCode, 400, 'Should not coearce the string into array') 744 | t.equal(res2.json().message, 'headers/x-another must be integer') 745 | t.equal(res2.statusCode, 400, 'Should not coearce the string into integer') 746 | } catch (err) { 747 | t.error(err) 748 | } 749 | }) 750 | 751 | test('Should use default plugin validator as fallback', async t => { 752 | t.plan(3) 753 | let compileCalled = false 754 | const defaultAjv = new AJV({ coerceTypes: false }) 755 | const defaultCompile = defaultAjv.compile.bind(defaultAjv) 756 | 757 | defaultAjv.compile = schema => { 758 | compileCalled = true 759 | return defaultCompile(schema) 760 | } 761 | 762 | const proxiedPlugin = proxyquire('..', { 763 | ajv: class { 764 | constructor () { 765 | return defaultAjv 766 | } 767 | } 768 | }) 769 | 770 | const server = Fastify() 771 | 772 | server.addSchema({ 773 | $id: 'some', 774 | type: 'array', 775 | items: { 776 | type: 'string' 777 | } 778 | }) 779 | 780 | server.register((instance, opts, done) => { 781 | instance.addSchema({ 782 | $id: 'another', 783 | type: 'integer' 784 | }) 785 | 786 | instance.register(proxiedPlugin, {}) 787 | 788 | instance.post( 789 | '/', 790 | { 791 | schema: { 792 | query: { 793 | type: 'object', 794 | properties: { 795 | msg: { 796 | $ref: 'some#' 797 | } 798 | } 799 | }, 800 | headers: { 801 | type: 'object', 802 | properties: { 803 | 'x-another': { 804 | $ref: 'another#' 805 | } 806 | } 807 | } 808 | } 809 | }, 810 | (req, reply) => { 811 | reply.send({ noop: 'noop' }) 812 | } 813 | ) 814 | 815 | done() 816 | }) 817 | 818 | try { 819 | const res = await server.inject({ 820 | method: 'POST', 821 | url: '/', 822 | query: { 823 | msg: ['string'] 824 | } 825 | }) 826 | 827 | t.equal(res.json().message, 'querystring/msg must be array') 828 | t.equal(res.statusCode, 400, 'Should not coearce the string into array') 829 | t.ok(compileCalled, 'Should have called the default Ajv instance') 830 | } catch (err) { 831 | t.error(err) 832 | } 833 | }) 834 | 835 | test('Should always cache schema to default plugin validator', async t => { 836 | t.plan(4) 837 | let compileCalled = false 838 | let customCompileCalled = false 839 | const defaultAjv = new AJV({ coerceTypes: false }) 840 | const headerAjv = new AJV({ coerceTypes: false }) 841 | const defaultCompile = defaultAjv.compile.bind(defaultAjv) 842 | const headerDefaultCompile = headerAjv.compile.bind(headerAjv) 843 | 844 | defaultAjv.compile = schema => { 845 | compileCalled = true 846 | return defaultCompile(schema) 847 | } 848 | 849 | headerAjv.compile = schema => { 850 | console.log('called') 851 | customCompileCalled = true 852 | return headerDefaultCompile(schema) 853 | } 854 | 855 | const proxiedPlugin = proxyquire('..', { 856 | ajv: class { 857 | constructor () { 858 | return defaultAjv 859 | } 860 | } 861 | }) 862 | 863 | const server = Fastify() 864 | 865 | server.addSchema({ 866 | $id: 'some', 867 | type: 'array', 868 | items: { 869 | type: 'string' 870 | } 871 | }) 872 | 873 | server.register(async (instance, opts) => { 874 | instance.addSchema({ 875 | $id: 'another', 876 | type: 'integer' 877 | }) 878 | 879 | await instance.register(proxiedPlugin, {}) 880 | 881 | instance.post( 882 | '/', 883 | { 884 | schema: { 885 | query: { 886 | type: 'object', 887 | properties: { 888 | msg: { 889 | $ref: 'some#' 890 | } 891 | } 892 | }, 893 | headers: { 894 | type: 'object', 895 | properties: { 896 | 'x-another': { 897 | $ref: 'another#' 898 | } 899 | } 900 | } 901 | }, 902 | config: { 903 | schemaValidators: { 904 | headers: headerAjv 905 | } 906 | } 907 | }, 908 | (req, reply) => { 909 | reply.send({ noop: 'noop' }) 910 | } 911 | ) 912 | }) 913 | 914 | try { 915 | const res = await server.inject({ 916 | method: 'POST', 917 | url: '/', 918 | query: { 919 | msg: ['string'] 920 | }, 921 | headers: { 922 | 'x-another': 1 923 | } 924 | }) 925 | 926 | t.equal(res.json().message, 'querystring/msg must be array') 927 | t.equal(res.statusCode, 400, 'Should not coearce the string into array') 928 | t.ok(compileCalled, 'Should have called the default Ajv instance') 929 | t.ok(customCompileCalled, 'Should have called the custom Ajv instance') 930 | } catch (err) { 931 | t.error(err) 932 | } 933 | }) 934 | 935 | test('Should use default provided validator as fallback', async t => { 936 | t.plan(3) 937 | let compileCalled = false 938 | const defaultAjv = new AJV({ coerceTypes: false }) 939 | const defaultCompile = defaultAjv.compile.bind(defaultAjv) 940 | 941 | defaultAjv.compile = schema => { 942 | compileCalled = true 943 | return defaultCompile(schema) 944 | } 945 | 946 | const server = Fastify() 947 | 948 | server.addSchema({ 949 | $id: 'some', 950 | type: 'array', 951 | items: { 952 | type: 'string' 953 | } 954 | }) 955 | 956 | server.register((instance, opts, done) => { 957 | instance.addSchema({ 958 | $id: 'another', 959 | type: 'integer' 960 | }) 961 | 962 | instance.register(plugin, { 963 | defaultValidator: defaultAjv 964 | }) 965 | 966 | instance.post( 967 | '/', 968 | { 969 | schema: { 970 | query: { 971 | type: 'object', 972 | properties: { 973 | msg: { 974 | $ref: 'some#' 975 | } 976 | } 977 | }, 978 | headers: { 979 | type: 'object', 980 | properties: { 981 | 'x-another': { 982 | $ref: 'another#' 983 | } 984 | } 985 | } 986 | } 987 | }, 988 | (req, reply) => { 989 | reply.send({ noop: 'noop' }) 990 | } 991 | ) 992 | 993 | done() 994 | }) 995 | 996 | try { 997 | const res = await server.inject({ 998 | method: 'POST', 999 | url: '/', 1000 | query: { 1001 | msg: ['string'] 1002 | } 1003 | }) 1004 | 1005 | t.equal(res.json().message, 'querystring/msg must be array') 1006 | t.equal(res.statusCode, 400, 'Should not coearce the string into array') 1007 | t.ok(compileCalled, 'Should have called the default Ajv instance') 1008 | } catch (err) { 1009 | t.error(err) 1010 | } 1011 | }) 1012 | --------------------------------------------------------------------------------