diff --git a/.github/ISSUE_TEMPLATE/bug-report---.md b/.github/ISSUE_TEMPLATE/bug-report---.md
new file mode 100644
index 0000000..947fa29
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug-report---.md
@@ -0,0 +1,38 @@
+---
+name: "Bug report \U0001F41B"
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..dd84ea7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,38 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
+1. Go to '...'
+2. Click on '....'
+3. Scroll down to '....'
+4. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots**
+If applicable, add screenshots to help explain your problem.
+
+**Desktop (please complete the following information):**
+ - OS: [e.g. iOS]
+ - Browser [e.g. chrome, safari]
+ - Version [e.g. 22]
+
+**Smartphone (please complete the following information):**
+ - Device: [e.g. iPhone6]
+ - OS: [e.g. iOS8.1]
+ - Browser [e.g. stock browser, safari]
+ - Version [e.g. 22]
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature-request---.md b/.github/ISSUE_TEMPLATE/feature-request---.md
new file mode 100644
index 0000000..350935b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature-request---.md
@@ -0,0 +1,20 @@
+---
+name: "Feature request \U0001F916"
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..bbcbbe7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/stale.yml b/.github/stale.yml
new file mode 100644
index 0000000..d51ce63
--- /dev/null
+++ b/.github/stale.yml
@@ -0,0 +1,21 @@
+# Number of days of inactivity before an issue becomes stale
+daysUntilStale: 15
+# Number of days of inactivity before a stale issue is closed
+daysUntilClose: 7
+# Issues with these labels will never be considered stale
+exemptLabels:
+ - "discussion"
+ - "feature request"
+ - "bug"
+ - "help wanted"
+ - "plugin suggestion"
+ - "good first issue"
+# Label to use when marking an issue as stale
+staleLabel: stale
+# Comment to post when marking an issue as stale. Set to `false` to disable
+markComment: >
+ This issue has been automatically marked as stale because it has not had
+ recent activity. It will be closed if no further activity occurs. Thank you
+ for your contributions.
+# Comment to post when closing a stale issue. Set to `false` to disable
+closeComment: false
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..8e603fc
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,20 @@
+name: CI
+on: [push, pull_request]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node: [14.x, 16.x, 17.x]
+ name: Node ${{ matrix.node }}
+ steps:
+ - uses: actions/checkout@v1
+ - name: Setup node
+ uses: actions/setup-node@v1
+ with:
+ node-version: ${{ matrix.node }}
+ - run: npm install
+ - run: npm run lint:ci
+ - run: npm run test:ci
+ - run: npm run typescript
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dbba6f5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,107 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and *not* Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# package-lock.json
+package-lock.json
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100755
index 0000000..1dc4645
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,4 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+npm run test:ci
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..48143fc
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Carlos Fuentes
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 857c0fd..082f5c0 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-# fastify-split-validator
\ No newline at end of file
+# fastify-split-validator
diff --git a/index.d.ts b/index.d.ts
new file mode 100644
index 0000000..6fba6ae
--- /dev/null
+++ b/index.d.ts
@@ -0,0 +1,23 @@
+///
+import { FastifyPluginCallback, FastifyContextConfig } from 'fastify';
+import Ajv from 'ajv';
+
+interface FastifySplitValidator {
+ defaultValidator?: Ajv;
+}
+
+declare module 'fastify' {
+ interface FastifyContextConfig {
+ schemaValidators: {
+ body?: Ajv;
+ headers?: Ajv;
+ querystring?: Ajv;
+ query?: Ajv;
+ params?: Ajv;
+ };
+ }
+}
+
+declare const FastifySplitValidator: FastifyPluginCallback;
+
+export default FastifySplitValidator;
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..0e41022
--- /dev/null
+++ b/index.js
@@ -0,0 +1,83 @@
+'use strict'
+
+const fp = require('fastify-plugin')
+const Ajv = require('ajv')
+const ValidatorDictionary = require('./lib/dictionary')
+function plugin (fastifyInstance, opts = {}, done) {
+ let { defaultValidator } = opts
+
+ // validation and more
+ const dictionary = new ValidatorDictionary()
+ fastifyInstance.addHook('onRoute', params => {
+ if (params.config?.schemaValidators != null) {
+ const { path, method, config } = params
+ const compilers = config.schemaValidators
+ const keys = Object.keys(compilers)
+
+ for (const key of keys) {
+ dictionary.addValidator(
+ path,
+ method,
+ // If query passed, we should change it to querystring
+ key === 'query' ? 'querystring' : key,
+ compilers[key]
+ )
+ }
+ }
+ })
+
+ fastifyInstance.setSchemaController({
+ compilersFactory: {
+ // TODO: Maybe the same for serializer?
+ buildValidator: function (externalSchemas, ajvServerOptions) {
+ // We load schemas if any
+ const schemaIds =
+ externalSchemas != null ? Object.keys(externalSchemas) : []
+ defaultValidator = defaultValidator ?? new Ajv(ajvServerOptions)
+
+ if (schemaIds.length > 0) {
+ const validators = dictionary.getValidators()
+
+ for (const schemaKey of schemaIds) {
+ const schema = externalSchemas[schemaKey]
+ for (const validator of validators) {
+ // Check if schema added or not for custom validators
+ if (validator.getSchema(schemaKey) == null) {
+ validator.addSchema(schema, schemaKey)
+ }
+ }
+
+ // Also add it to default validator as fallback
+ if (defaultValidator.getSchema(schemaKey) == null) {
+ console.log('adding to default validator', schemaKey)
+ defaultValidator.addSchema(schema, schemaKey)
+ }
+ }
+ }
+
+ return function validatorCompiler ({ schema, method, url, httpPart }) {
+ const httpPartValidator = dictionary.getValidator(
+ url,
+ method,
+ httpPart
+ )
+ // We compile for cache all schemas for performance
+ const fallback = defaultValidator.compile(schema)
+
+ if (httpPartValidator == null) {
+ return fallback
+ }
+
+ return httpPartValidator.compile(schema)
+ }
+ }
+ }
+ })
+
+ done()
+}
+
+module.exports = fp(plugin, {
+ fastify: '>=3.24.1',
+ name: 'fastify-split-validator'
+})
diff --git a/lib/dictionary.js b/lib/dictionary.js
new file mode 100644
index 0000000..ad7fddf
--- /dev/null
+++ b/lib/dictionary.js
@@ -0,0 +1,46 @@
+const Ajv = require('ajv')
+const { kSchemas } = require('./symbols')
+
+function flatSchemas (schemas) {
+ const keys = Object.keys(schemas)
+
+ if (keys.length === 0) return []
+
+ let result = []
+ for (const key of keys) {
+ if (schemas[key] instanceof Ajv) {
+ result.push(schemas[key])
+ } else {
+ const nestedSchemas = flatSchemas(schemas[key])
+ result = result.concat(nestedSchemas)
+ }
+ }
+
+ return result
+}
+
+module.exports = class ValidatorDictionary {
+ constructor () {
+ this[kSchemas] = {}
+ }
+
+ addValidator (path, method, httpPart, validator) {
+ if (this[kSchemas][path] == null) {
+ this[kSchemas][path] = {
+ [method]: {
+ [httpPart]: validator
+ }
+ }
+ } else {
+ this[kSchemas][path][method][httpPart] = validator
+ }
+ }
+
+ getValidator (path, method, httpPart) {
+ return this[kSchemas][path]?.[method]?.[httpPart]
+ }
+
+ getValidators () {
+ return flatSchemas(this[kSchemas])
+ }
+}
diff --git a/lib/symbols.js b/lib/symbols.js
new file mode 100644
index 0000000..cbc50b8
--- /dev/null
+++ b/lib/symbols.js
@@ -0,0 +1,3 @@
+module.exports = {
+ kSchemas: Symbol.for('#schemas')
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..92f3a99
--- /dev/null
+++ b/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "t",
+ "version": "0.0.0",
+ "description": "Validate each HTTP body message part and Queryparams with different AJV schemas",
+ "main": "index.js",
+ "types": "index.d.ts",
+ "scripts": {
+ "test": "tap --cov test/*.test.js && npm run typescript",
+ "test:ci": "tap --cov test/*.test.js && npm run typescript && npm run lint",
+ "test:only": "tap --only",
+ "test:unit": "tap test/*.test.js",
+ "lint": "standard | snazzy",
+ "lint:ci": "standard",
+ "typescript": "tsd"
+ },
+ "keywords": [
+ "fastify",
+ "ajv",
+ "validator",
+ "fastify-split-validator"
+ ],
+ "author": "MetCoder95 ",
+ "license": "MIT",
+ "devDependencies": {
+ "@types/node": "^14.17.6",
+ "fastify": "^3.24.1",
+ "husky": "^7.0.2",
+ "proxyquire": "^2.1.3",
+ "snazzy": "^9.0.0",
+ "standard": "^16.0.3",
+ "tap": "^15.0.10",
+ "tsd": "^0.17.0",
+ "typescript": "^4.4"
+ },
+ "dependencies": {
+ "ajv": "^8.6.3",
+ "fastify-plugin": "^3.0.0"
+ },
+ "tsd": {
+ "directory": "test"
+ },
+ "tap": {
+ "check-coverage": false
+ },
+ "standard": {
+ "ignore": [
+ "*.d.ts",
+ "*.test-d.ts"
+ ]
+ }
+}
diff --git a/test/index.test-d.ts b/test/index.test-d.ts
new file mode 100644
index 0000000..dac68a6
--- /dev/null
+++ b/test/index.test-d.ts
@@ -0,0 +1,136 @@
+import fastify from 'fastify'
+import Ajv from 'ajv'
+import plugin from '..'
+
+const serverHttp = fastify()
+const customAjv = new Ajv()
+
+serverHttp.register(plugin)
+
+serverHttp.addSchema({
+ $id: 'hello',
+ type: 'string'
+})
+
+serverHttp.register(plugin, {
+ defaultValidator: customAjv
+})
+
+serverHttp.get('/', {
+ schema: {
+ querystring: {
+ hello: {
+ $ref: 'hello#'
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ querystring: customAjv
+ }
+ }
+} , async (request, reply) => {})
+
+
+// -> Second level
+serverHttp.register(
+ function (fastifyInstance, opts, done) {
+ fastifyInstance.register(plugin)
+
+ fastifyInstance.get('/string',{
+ schema:{
+ params: {
+ hello: {
+ $ref: 'hello#',
+ }
+ },
+ response:{
+ 200:{
+ type:'string'
+ }
+ },
+ },
+ config: {
+ schemaValidators: {
+ params: customAjv
+ }
+ }
+ }, (req, reply) => {
+ reply.send({
+ hello: 'world'
+ })
+ })
+
+ // Sending a JSON
+ serverHttp.post('/json', (req, reply) => {
+ reply.send({ foo: 'bar' })
+ })
+
+
+ done()
+ },
+ { prefix: '/api' }
+)
+
+const serverHttp2 = fastify({ http2: true })
+serverHttp2.addSchema({
+ $id: 'hello',
+ type: 'string'
+})
+
+
+serverHttp2.register(plugin, {
+ defaultValidator: new Ajv()
+})
+
+serverHttp2.get('/', {
+ schema: {
+ querystring: {
+ hello: {
+ $ref: 'hello#'
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ querystring: customAjv
+ }
+ }
+} , async (request, reply) => {})
+
+// -> First plugin
+serverHttp2.register(
+ function (fastifyInstance, opts, done) {
+ fastifyInstance.get('/string',{
+ schema:{
+ params: {
+ hello: {
+ $ref: 'hello#',
+ }
+ },
+ response:{
+ 200:{
+ type:'string'
+ }
+ },
+ },
+ config: {
+ schemaValidators: {
+ params: customAjv
+ }
+ }
+ }, (req, reply) => {
+ reply.send({
+ hello: 'world'
+ })
+ })
+
+ // Sending a JSON
+ fastifyInstance.post('/json', (req, reply) => {
+ reply.send({ foo: 'bar' })
+ })
+
+ done()
+ },
+ { prefix: '/api' }
+)
diff --git a/test/index.test.js b/test/index.test.js
new file mode 100644
index 0000000..2353413
--- /dev/null
+++ b/test/index.test.js
@@ -0,0 +1,957 @@
+'use strict'
+
+const tap = require('tap')
+const AJV = require('ajv')
+const Fastify = require('fastify')
+const proxyquire = require('proxyquire')
+const plugin = require('..')
+
+const test = tap.test
+
+tap.plan(15)
+
+test('Should allow custom AJV instance for querystring', async t => {
+ t.plan(1)
+ const customAjv = new AJV({ coerceTypes: false })
+ const server = Fastify()
+
+ server.register(plugin, {})
+
+ server.get(
+ '/',
+ {
+ schema: {
+ querystring: {
+ msg: {
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ // TODO: normalize to query
+ querystring: customAjv
+ }
+ }
+ },
+ (req, reply) => {}
+ )
+
+ try {
+ const res = await server.inject({
+ method: 'GET',
+ url: '/',
+ query: {
+ msg: ['hello world']
+ }
+ })
+
+ t.equal(
+ res.statusCode,
+ 400,
+ 'Should coerce the single element array into string'
+ )
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should allow custom AJV instance for body', async t => {
+ t.plan(2)
+ const customAjv = new AJV({ coerceTypes: false })
+ const server = Fastify()
+
+ server.register(plugin, {})
+
+ server.post(
+ '/',
+ {
+ schema: {
+ body: {
+ type: 'object',
+ properties: {
+ msg: {
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ }
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ body: customAjv
+ }
+ }
+ },
+ (req, reply) => {}
+ )
+
+ try {
+ const res = await server.inject({
+ method: 'POST',
+ url: '/',
+ payload: {
+ msg: 'hello world'
+ }
+ })
+
+ const body = res.json()
+
+ t.equal(body.message, 'body must be array')
+ t.equal(
+ res.statusCode,
+ 400,
+ 'Should coerce the single element array into string'
+ )
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should allow custom AJV instance for params', async t => {
+ t.plan(2)
+ const customAjv = new AJV({ coerceTypes: false })
+ const server = Fastify()
+
+ server.register(plugin, {})
+
+ server.get(
+ '/:msg',
+ {
+ schema: {
+ params: {
+ msg: {
+ type: 'integer'
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ params: customAjv
+ }
+ }
+ },
+ (req, reply) => {}
+ )
+
+ try {
+ const res = await server.inject({
+ method: 'GET',
+ url: '/1'
+ })
+
+ const body = res.json()
+
+ t.equal(body.message, 'params must be integer')
+ t.equal(
+ res.statusCode,
+ 400,
+ 'Should coerce the single element array into string'
+ )
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should allow custom AJV instance for headers', async t => {
+ t.plan(2)
+ const customAjv = new AJV({ coerceTypes: false })
+ const server = Fastify()
+
+ server.register(plugin, {})
+
+ server.get(
+ '/',
+ {
+ schema: {
+ headers: {
+ 'x-type': {
+ type: 'integer'
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ headers: customAjv
+ }
+ }
+ },
+ (req, reply) => {}
+ )
+
+ try {
+ const res = await server.inject({
+ method: 'GET',
+ path: '/',
+ headers: {
+ 'x-type': '1'
+ }
+ })
+
+ const body = res.json()
+
+ // TODO: set into documentation that it's possible the
+ // error formatter doesn't work as expected.
+ // Custom one should be provided
+ t.equal(body.message, 'headers must be integer')
+ t.equal(
+ res.statusCode,
+ 400,
+ 'Should coerce the single element array into string'
+ )
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should work with referenced schemas (querystring)', async t => {
+ t.plan(2)
+ const customAjv = new AJV({ coerceTypes: false })
+ const server = Fastify()
+
+ server.addSchema({
+ $id: 'some',
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ })
+
+ server.register(plugin, {})
+
+ // The issue is at the `Fastify#setSchemaControler` level,
+ // as when adding a new SchemaController the parent is passed
+ // instead of the same old Schema Controller, causing
+ // to lose the reference to the prior registered Schemas.
+ // Reported at: https://github.com/fastify/fastify/issues/3121
+ server.get(
+ '/',
+ {
+ schema: {
+ query: {
+ msg: {
+ $ref: 'some#'
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ querystring: customAjv
+ }
+ }
+ },
+ (req, reply) => {
+ reply.send({ noop: 'noop' })
+ }
+ )
+
+ try {
+ const res = await server.inject({
+ method: 'GET',
+ url: '/',
+ query: {
+ msg: ['hello world']
+ }
+ })
+
+ const body = res.json()
+
+ t.equal(body.message, 'querystring must be array')
+ t.equal(
+ res.statusCode,
+ 400,
+ 'Should parse the single element array into string'
+ )
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should work with referenced schemas (params)', async t => {
+ t.plan(2)
+ const customAjv = new AJV({ coerceTypes: false })
+ const server = Fastify()
+
+ server.addSchema({
+ $id: 'some',
+ type: 'integer'
+ })
+
+ server.register(plugin, {})
+
+ server.get(
+ '/:id',
+ {
+ schema: {
+ params: {
+ id: {
+ $ref: 'some#'
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ params: customAjv
+ }
+ }
+ },
+ (req, reply) => {
+ reply.send({ noop: 'noop' })
+ }
+ )
+
+ try {
+ const res = await server.inject({
+ method: 'GET',
+ url: '/1'
+ })
+
+ const body = res.json()
+
+ t.equal(body.message, 'params must be integer')
+ t.equal(res.statusCode, 400, 'Should not coearce the string into integer')
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should work with referenced schemas (headers)', async t => {
+ t.plan(2)
+ const customAjv = new AJV({ coerceTypes: false })
+ const server = Fastify()
+
+ server.addSchema({
+ $id: 'some',
+ type: 'integer'
+ })
+
+ server.register(plugin, {})
+
+ server.get(
+ '/',
+ {
+ schema: {
+ headers: {
+ 'x-id': {
+ $ref: 'some#'
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ headers: customAjv
+ }
+ }
+ },
+ (req, reply) => {
+ reply.send({ noop: 'noop' })
+ }
+ )
+
+ try {
+ const res = await server.inject({
+ method: 'GET',
+ url: '/',
+ headers: {
+ 'x-id': '1'
+ }
+ })
+
+ const body = res.json()
+
+ t.equal(body.message, 'headers must be integer')
+ t.equal(res.statusCode, 400, 'Should not coearce the string into integer')
+ } catch (err) {
+ t.error(err)
+ }
+})
+test('Should work with referenced schemas (body)', async t => {
+ t.plan(2)
+ const customAjv = new AJV({ coerceTypes: false })
+ const server = Fastify()
+
+ server.addSchema({
+ $id: 'some',
+ type: 'string'
+ })
+
+ server.register(plugin, {})
+
+ server.post(
+ '/',
+ {
+ schema: {
+ body: {
+ type: 'object',
+ properties: {
+ msg: {
+ $ref: 'some#'
+ }
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ body: customAjv
+ }
+ }
+ },
+ (req, reply) => {
+ reply.send({ noop: 'noop' })
+ }
+ )
+
+ try {
+ const res = await server.inject({
+ method: 'POST',
+ url: '/',
+ payload: {
+ msg: 1
+ }
+ })
+
+ const body = res.json()
+
+ t.equal(body.message, 'body must be string')
+ t.equal(res.statusCode, 400, 'Should not coearce the string into integer')
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should work with parent and same instance schemas', async t => {
+ t.plan(2)
+ const customAjv = new AJV({ coerceTypes: false })
+ const server = Fastify()
+
+ server.addSchema({
+ $id: 'some',
+ type: 'string'
+ })
+
+ server.register((instance, opts, done) => {
+ instance.addSchema({
+ $id: 'another',
+ type: 'string'
+ })
+
+ // #TODO: Another bug, schemas defined within the same
+ // encaptulated plugin are not being registered
+ instance.register(plugin, {})
+
+ instance.post(
+ '/',
+ {
+ schema: {
+ body: {
+ type: 'object',
+ properties: {
+ msg: {
+ $ref: 'some#'
+ },
+ another: {
+ $ref: 'another#'
+ }
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ body: customAjv
+ }
+ }
+ },
+ (req, reply) => {
+ reply.send({ noop: 'noop' })
+ }
+ )
+
+ done()
+ })
+
+ try {
+ const res = await server.inject({
+ method: 'POST',
+ url: '/',
+ payload: {
+ msg: 1
+ }
+ })
+
+ const body = res.json()
+
+ t.equal(body.message, 'body must be string')
+ t.equal(res.statusCode, 400, 'Should not coearce the string into integer')
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should work with parent schemas', async t => {
+ t.plan(2)
+ const customAjv = new AJV({ coerceTypes: false })
+ const server = Fastify()
+
+ server.addSchema({
+ $id: 'some',
+ type: 'string'
+ })
+
+ server.register((instance, opts, done) => {
+ instance.register(plugin, {})
+
+ instance.post(
+ '/',
+ {
+ schema: {
+ body: {
+ type: 'object',
+ properties: {
+ msg: {
+ $ref: 'some#'
+ }
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ body: customAjv
+ }
+ }
+ },
+ (req, reply) => {
+ reply.send({ noop: 'noop' })
+ }
+ )
+
+ done()
+ })
+
+ try {
+ const res = await server.inject({
+ method: 'POST',
+ url: '/',
+ payload: {
+ msg: 1
+ }
+ })
+
+ const body = res.json()
+
+ t.equal(body.message, 'body must be string')
+ t.equal(res.statusCode, 400, 'Should not coearce the string into integer')
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should work with parent nested schemas', async t => {
+ t.plan(4)
+ const customAjv = new AJV({ coerceTypes: false })
+ const server = Fastify()
+
+ server.addSchema({
+ $id: 'some',
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ })
+
+ server.register((instance, opts, done) => {
+ instance.addSchema({
+ $id: 'another',
+ type: 'integer'
+ })
+
+ instance.register((subInstance, opts, innerDone) => {
+ subInstance.register(plugin, {})
+
+ subInstance.post(
+ '/',
+ {
+ schema: {
+ querystring: {
+ msg: {
+ $ref: 'some#'
+ }
+ },
+ headers: {
+ 'x-another': {
+ $ref: 'another#'
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ querystring: customAjv,
+ headers: customAjv
+ }
+ }
+ },
+ (req, reply) => {
+ reply.send({ noop: 'noop' })
+ }
+ )
+
+ innerDone()
+ })
+
+ done()
+ })
+
+ try {
+ const [res1, res2] = await Promise.all([
+ server.inject({
+ method: 'POST',
+ url: '/',
+ query: {
+ msg: ['string']
+ }
+ }),
+ server.inject({
+ method: 'POST',
+ url: '/',
+ headers: {
+ 'x-another': '1'
+ }
+ })
+ ])
+
+ t.equal(res1.json().message, 'querystring must be array')
+ t.equal(res1.statusCode, 400, 'Should not coearce the string into array')
+ t.equal(res2.json().message, 'headers must be integer')
+ t.equal(res2.statusCode, 400, 'Should not coearce the string into integer')
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should handle parsing to querystring (query)', async t => {
+ t.plan(4)
+ const customAjv = new AJV({ coerceTypes: false })
+ const server = Fastify()
+
+ server.addSchema({
+ $id: 'some',
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ })
+
+ server.register((instance, opts, done) => {
+ instance.addSchema({
+ $id: 'another',
+ type: 'integer'
+ })
+
+ instance.register((subInstance, opts, innerDone) => {
+ subInstance.register(plugin, {})
+
+ subInstance.post(
+ '/',
+ {
+ schema: {
+ query: {
+ msg: {
+ $ref: 'some#'
+ }
+ },
+ headers: {
+ 'x-another': {
+ $ref: 'another#'
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ query: customAjv,
+ headers: customAjv
+ }
+ }
+ },
+ (req, reply) => {
+ reply.send({ noop: 'noop' })
+ }
+ )
+
+ innerDone()
+ })
+
+ done()
+ })
+
+ try {
+ const [res1, res2] = await Promise.all([
+ server.inject({
+ method: 'POST',
+ url: '/',
+ query: {
+ msg: ['string']
+ }
+ }),
+ server.inject({
+ method: 'POST',
+ url: '/',
+ headers: {
+ 'x-another': '1'
+ }
+ })
+ ])
+
+ t.equal(res1.json().message, 'querystring must be array')
+ t.equal(res1.statusCode, 400, 'Should not coearce the string into array')
+ t.equal(res2.json().message, 'headers must be integer')
+ t.equal(res2.statusCode, 400, 'Should not coearce the string into integer')
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should use default plugin validator as fallback', async t => {
+ t.plan(3)
+ let compileCalled = false
+ const defaultAjv = new AJV({ coerceTypes: false })
+ const defaultCompile = defaultAjv.compile.bind(defaultAjv)
+
+ defaultAjv.compile = schema => {
+ compileCalled = true
+ return defaultCompile(schema)
+ }
+
+ const proxiedPlugin = proxyquire('..', {
+ ajv: class {
+ constructor () {
+ return defaultAjv
+ }
+ }
+ })
+
+ const server = Fastify()
+
+ server.addSchema({
+ $id: 'some',
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ })
+
+ server.register((instance, opts, done) => {
+ instance.addSchema({
+ $id: 'another',
+ type: 'integer'
+ })
+
+ instance.register(proxiedPlugin, {})
+
+ instance.post(
+ '/',
+ {
+ schema: {
+ query: {
+ msg: {
+ $ref: 'some#'
+ }
+ },
+ headers: {
+ 'x-another': {
+ $ref: 'another#'
+ }
+ }
+ }
+ },
+ (req, reply) => {
+ reply.send({ noop: 'noop' })
+ }
+ )
+
+ done()
+ })
+
+ try {
+ const res = await server.inject({
+ method: 'POST',
+ url: '/',
+ query: {
+ msg: ['string']
+ }
+ })
+
+ t.equal(res.json().message, 'querystring must be array')
+ t.equal(res.statusCode, 400, 'Should not coearce the string into array')
+ t.ok(compileCalled, 'Should have called the default Ajv instance')
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should always cache schema to default plugin validator', async t => {
+ t.plan(4)
+ let compileCalled = false
+ let customCompileCalled = false
+ const defaultAjv = new AJV({ coerceTypes: false })
+ const headerAjv = new AJV({ coerceTypes: false })
+ const defaultCompile = defaultAjv.compile.bind(defaultAjv)
+ const headerDefaultCompile = headerAjv.compile.bind(headerAjv)
+
+ defaultAjv.compile = schema => {
+ compileCalled = true
+ return defaultCompile(schema)
+ }
+
+ headerAjv.compile = schema => {
+ customCompileCalled = true
+ return headerDefaultCompile(schema)
+ }
+
+ const proxiedPlugin = proxyquire('..', {
+ ajv: class {
+ constructor () {
+ return defaultAjv
+ }
+ }
+ })
+
+ const server = Fastify()
+
+ server.addSchema({
+ $id: 'some',
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ })
+
+ server.register((instance, opts, done) => {
+ instance.addSchema({
+ $id: 'another',
+ type: 'integer'
+ })
+
+ instance.register(proxiedPlugin, {})
+
+ instance.post(
+ '/',
+ {
+ schema: {
+ query: {
+ msg: {
+ $ref: 'some#'
+ }
+ },
+ headers: {
+ 'x-another': {
+ $ref: 'another#'
+ }
+ }
+ },
+ config: {
+ schemaValidators: {
+ headers: headerAjv
+ }
+ }
+ },
+ (req, reply) => {
+ reply.send({ noop: 'noop' })
+ }
+ )
+
+ done()
+ })
+
+ try {
+ const res = await server.inject({
+ method: 'POST',
+ url: '/',
+ query: {
+ msg: ['string']
+ }
+ })
+
+ t.equal(res.json().message, 'querystring must be array')
+ t.equal(res.statusCode, 400, 'Should not coearce the string into array')
+ t.ok(compileCalled, 'Should have called the default Ajv instance')
+ t.ok(customCompileCalled, 'Should have called the custom Ajv instance')
+ } catch (err) {
+ t.error(err)
+ }
+})
+
+test('Should use default provided validator as fallback', async t => {
+ t.plan(3)
+ let compileCalled = false
+ const defaultAjv = new AJV({ coerceTypes: false })
+ const defaultCompile = defaultAjv.compile.bind(defaultAjv)
+
+ defaultAjv.compile = schema => {
+ compileCalled = true
+ return defaultCompile(schema)
+ }
+
+ const server = Fastify()
+
+ server.addSchema({
+ $id: 'some',
+ type: 'array',
+ items: {
+ type: 'string'
+ }
+ })
+
+ server.register((instance, opts, done) => {
+ instance.addSchema({
+ $id: 'another',
+ type: 'integer'
+ })
+
+ instance.register(plugin, {
+ defaultValidator: defaultAjv
+ })
+
+ instance.post(
+ '/',
+ {
+ schema: {
+ query: {
+ msg: {
+ $ref: 'some#'
+ }
+ },
+ headers: {
+ 'x-another': {
+ $ref: 'another#'
+ }
+ }
+ }
+ },
+ (req, reply) => {
+ reply.send({ noop: 'noop' })
+ }
+ )
+
+ done()
+ })
+
+ try {
+ const res = await server.inject({
+ method: 'POST',
+ url: '/',
+ query: {
+ msg: ['string']
+ }
+ })
+
+ t.equal(res.json().message, 'querystring must be array')
+ t.equal(res.statusCode, 400, 'Should not coearce the string into array')
+ t.ok(compileCalled, 'Should have called the default Ajv instance')
+ } catch (err) {
+ t.error(err)
+ }
+})