├── .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 | [](https://github.com/MetCoder95/fastify-split-validator/actions/workflows/ci.yml) [](https://github.com/MetCoder95/fastify-split-validator/actions/workflows/codeql-analysis.yml) 
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 |
--------------------------------------------------------------------------------