diff --git a/README.md b/README.md index 4a75d40d..58ad1389 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,51 @@ fastify.ready(err => { fastify.swagger() }) ``` + + +## OpenAPI validation formats + +There are three way to enable the use of OpenAPI validation formats: + +1) ### Project Level + +```js +const fastify = require('fastify')() + +fastify.register(require('fastify-swagger'), { + customCompiler: true +}) +``` + +2) ### Instance Level + +```js +const{ fastifySwagger, validatorCompiler } = require("fastify-swagger"); + +fastify.setValidatorCompiler(validatorCompiler) +``` + +2) ### Route Level + +```js +const{ fastifySwagger, validatorCompiler } = require("fastify-swagger"); + +fastify.get( + '/', + { + schema: { + body: { + type: 'object', + properties: { file: { type: 'string', format: 'binary' } } + } + }, + validatorCompiler + }, + ( ) => {} +) +``` + + ## API @@ -218,6 +263,7 @@ An example of using `fastify-swagger` with `static` mode enabled can be found [h | Option | Default | Description | | ------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------- | | exposeRoute | false | Exposes documentation route. | + | customCompiler | false | If `true` enable OpenAPI validation formats (binary, byte, int32, int64) as a schema | | hiddenTag | X-HIDDEN | Tag to control hiding of routes. | | hideUntagged | false | If `true` remove routes without tags from resulting Swagger/OpenAPI schema file. | | initOAuth | {} | Configuration options for [Swagger UI initOAuth](https://swagger.io/docs/open-source-tools/swagger-ui/usage/oauth2/). | @@ -447,6 +493,8 @@ Please specify `type: 'null'` for the response otherwise Fastify itself will fai } ``` + + #### OpenAPI Parameter Options @@ -702,11 +750,42 @@ You can integration this plugin with ```fastify-helmet``` with some little work. }) ``` + + +### upload File Schema + +1) Enable open api validation formats (`docs available in upper`) + +2) FileUpload Schema + +```javascript + +async function routes(fastify, opts, next){ + fastify.post("/upload", { + schema: { + type: "object", + body: { + type: "object", + properties: { + file: { + type: "file", + format: "binary" + } + } + } + + }}, ()=>{}) +} + +``` + ## `$id` and `$ref` usage + +### Development + -## Development In order to start development run: ``` npm i diff --git a/examples/options.js b/examples/options.js index ea31100a..5d8c3c0c 100644 --- a/examples/options.js +++ b/examples/options.js @@ -30,6 +30,7 @@ const swaggerOption = { } const openapiOption = { + customCompiler: true, openapi: { info: { title: 'Test swagger', diff --git a/index.js b/index.js index 04348477..577c2974 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,15 @@ 'use strict' const fp = require('fastify-plugin') +const { validatorCompiler } = require('./lib/validatorCompiler') function fastifySwagger (fastify, opts, next) { + // enabling custom or validator complier form opts object + const customCompiler = opts.customCompiler || null + if (customCompiler) { + fastify.setValidatorCompiler(validatorCompiler) + } + // by default the mode is dynamic, as plugin initially was developed opts.mode = opts.mode || 'dynamic' @@ -25,7 +32,10 @@ function fastifySwagger (fastify, opts, next) { fastify.decorate('swaggerCSP', require('./static/csp.json')) } -module.exports = fp(fastifySwagger, { +const plugin = fp(fastifySwagger, { fastify: '>=3.x', name: 'fastify-swagger' }) + +module.exports = plugin +module.exports.validatorCompiler = validatorCompiler diff --git a/lib/validatorCompiler.js b/lib/validatorCompiler.js new file mode 100644 index 00000000..dc69fa60 --- /dev/null +++ b/lib/validatorCompiler.js @@ -0,0 +1,92 @@ +'use strict' + +const Ajv = require('ajv') +const Decimal = require('decimal.js') + +const range = { + + int64Bit: { + min: new Decimal('-9223372036854775808'), + max: new Decimal('9223372036854775807') + }, + int32Bit: { + min: new Decimal('-2147483648'), + max: new Decimal('2147483647') + }, + bytes: { + min: new Decimal('-128'), + max: new Decimal('127') + } + +} + +const binaryValidation = (data) => { + const binaryRegex = /^[0-1]{1,}$/g + return binaryRegex.test(data) +} + +const byteValidation = (data) => { + const notBase64 = /[^A-Z0-9+/=]/i + + const len = data.length + if (!len || len % 4 !== 0 || notBase64.test(data)) { + return false + } + + const firstPaddingChar = data.indexOf('=') + return firstPaddingChar === -1 || + firstPaddingChar === len - 1 || + (firstPaddingChar === len - 2 && data[len - 1] === '=') +} + +const int64bitValidation = (data) => { + return ( + Number.isInteger(+data) && + range.int64Bit.max.greaterThanOrEqualTo(data) && + range.int64Bit.min.lessThanOrEqualTo(data) + ) +} + +const int32bitValidation = (data) => { + return ( + Number.isInteger(+data) && + range.int32Bit.max.greaterThanOrEqualTo(data) && + range.int32Bit.min.lessThanOrEqualTo(data) + ) +} + +const validatorCompiler = (schema) => { + const ajv = new Ajv({ + removeAdditional: true, + useDefaults: true, + coerceTypes: true, + nullable: true + }) + + ajv.addFormat('binary', { + type: 'string', + validate: binaryValidation + }) + ajv.addFormat('byte', { + type: 'string', + validate: byteValidation + }) + ajv.addFormat('int32', { + type: 'number', + validate: int32bitValidation + }) + ajv.addFormat('int64', { + type: 'number', + validate: int64bitValidation + }) + + return ajv.compile(schema) +} + +module.exports = { + validatorCompiler, + int32bitValidation, + int64bitValidation, + binaryValidation, + byteValidation +} diff --git a/package.json b/package.json index 5f98c0ea..fa8a7ae8 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "tsd": "^0.19.0" }, "dependencies": { + "ajv": "^6.12.6", + "decimal.js": "^3.0.0", "fastify-plugin": "^3.0.0", "fastify-static": "^4.0.0", "js-yaml": "^4.0.0", diff --git a/test/esm/esm.mjs b/test/esm/esm.mjs index a2d7c450..820a0719 100644 --- a/test/esm/esm.mjs +++ b/test/esm/esm.mjs @@ -1,11 +1,11 @@ import t from 'tap' import Fastify from 'fastify' -import swaggerDefault from '../../index.js' +import fastifySwagger from '../../index.js' t.test('esm support', async t => { const fastify = Fastify() - fastify.register(swaggerDefault) + fastify.register(fastifySwagger) await fastify.ready() diff --git a/test/validatorCompiler.js b/test/validatorCompiler.js new file mode 100644 index 00000000..5e7aae66 --- /dev/null +++ b/test/validatorCompiler.js @@ -0,0 +1,128 @@ +'use strict' + +const { + test +} = require('tap') +const Fastify = require('fastify') +const { + openapiOption +} = require('../examples/options') + +const { + validatorCompiler, + fastifySwagger +} = require('../index') + +const { binaryValidation, byteValidation, int32bitValidation, int64bitValidation } = require('../lib/validatorCompiler') + +test('validator compiler is function', t => { + t.type(validatorCompiler, 'function') + t.ok('passed validator compiler is function') + t.end() +}) + +test('binary validation', t => { + const data = binaryValidation('0100110001') + if (data) { + t.pass() + } else { + t.fail() + } + t.end() +}) + +test('byte validation', t => { + const byteData = 'MTIz' + const data1 = byteValidation(byteData) + const data2 = byteValidation('abc') + + if (data1) { + t.pass() + } else { + t.fail() + } + + if (!data2) { + t.pass() + } else { + t.fail() + } + + const data3 = byteValidation('QCPvv6VAI++/pQ==') + + if (data3) { + t.pass() + } else { + t.fail() + } + t.end() +}) + +test('int 32 bit validation', t => { + const data = int32bitValidation('123') + if (data) { + t.pass() + } else { + t.fail() + } + t.end() +}) + +test('int 64 bit validation', t => { + const data = int64bitValidation('512') + if (data) { + t.pass() + } else { + t.fail() + } + t.end() +}) + +test('validator compiler working', t => { + const fastify = Fastify() + fastify.register(fastifySwagger, openapiOption) + fastify.post('/', { + schema: { + type: 'object', + consumes: ['multipart/form-data', 'application/json'], + body: { + type: 'object', + properties: { + fileName: { + type: 'string' + }, + upload: { + type: 'file', + format: 'binary' + } + } + } + } + + }, (req, res) => { + return { + status: 'success', + code: 200, + msg: 'operation completed successfully' + } + }) + + fastify.ready(() => { + fastify.inject({ + method: 'POST', + url: '/', + headers: { + 'content-type': 'application/json' + }, + payload: { + fileName: 'aditya_picture' + } + }, (err, res) => { + const data = res.json() + t.error(err) + t.equal(data.code, 200) + t.same(data, { status: 'success', code: 200, msg: 'operation completed successfully' }) + t.end() + }) + }) +})