api-schema-builder
Advanced tools
Comparing version 1.0.3 to 1.0.4
{ | ||
"name": "api-schema-builder", | ||
"version": "1.0.3", | ||
"version": "1.0.4", | ||
"description": "build schema with validators for each endpoint", | ||
@@ -22,7 +22,7 @@ "main": "src/index.js", | ||
}, | ||
"license": "MIT", | ||
"license": "Apache-2.0", | ||
"scripts": { | ||
"test": "node_modules/mocha/bin/_mocha ./test/*/*-test.js --recursive", | ||
"test:coverage": "nyc node_modules/mocha/bin/_mocha --recursive ./test/*/*-test.js ", | ||
"coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls", | ||
"test": "mocha --recursive", | ||
"test:coverage": "nyc npm test ", | ||
"coveralls": "cat ./coverage/lcov.info |./node_modules/.bin/coveralls", | ||
"lint": "./node_modules/.bin/eslint src" | ||
@@ -46,3 +46,3 @@ }, | ||
"homepage": "https://github.com/Zooz/api-schema-builder.git", | ||
"author": "Idan Tovi", | ||
"author": "Gal Cohen", | ||
"dependencies": { | ||
@@ -56,4 +56,4 @@ "ajv": "^6.6.2", | ||
"chai": "^4.2.0", | ||
"chai-sinon": "^2.8.1", | ||
"coveralls": "^3.0.2", | ||
"chai-as-promised": "^7.1.1", | ||
"coveralls": "^3.0.3", | ||
"eslint": "^5.15.1", | ||
@@ -66,8 +66,4 @@ "eslint-config-standard": "^12.0.0", | ||
"eslint-plugin-standard": "^4.0.0", | ||
"form-data": "^2.3.3", | ||
"lodash": "^4.17.11", | ||
"mocha": "^6.0.2", | ||
"nyc": "^13.3.0", | ||
"request": "^2.88.0", | ||
"sinon": "^4.5.0" | ||
"nyc": "^13.3.0" | ||
}, | ||
@@ -74,0 +70,0 @@ "publishConfig": { |
# api-schema-builder | ||
[![NPM Version](https://img.shields.io/npm/v/api-schema-builder.svg?style=flat)](https://npmjs.org/package/express-ajv-swagger-validation) | ||
[![Build Status](https://travis-ci.org/Zooz/api-schema-builder.svg?branch=master)](https://travis-ci.org/Zooz/api-schema-builder) | ||
[![Coverage Status](https://coveralls.io/repos/github/Zooz/api-schema-builder/badge.svg?branch=master)](https://coveralls.io/github/Zooz/api-schema-builder?branch=master) | ||
[![Known Vulnerabilities](https://snyk.io/test/npm/api-schema-builder/badge.svg)](https://snyk.io/test/npm/api-schema-builder) | ||
[![Apache 2.0 License](https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat)](LICENSE) | ||
@@ -43,3 +46,3 @@ This package is used to build schema for input validation base on openapi doc [Swagger (Open API)](https://swagger.io/specification/) definition and [ajv](https://www.npmjs.com/package/ajv) | ||
buildSchema the middleware with the swagger definition. | ||
Build schema that contains ajv validators for each endpoint, it base on swagger definition. | ||
@@ -51,8 +54,8 @@ The function return Promise. | ||
* `PathToSwaggerFile` – Path to the swagger definition | ||
* `options` – Additional options for the middleware. | ||
* `options` – Additional options for build the schema. | ||
#### Response | ||
Array that contains: | ||
* `path_name`: the paths it written in the doc, for example `/pet`. | ||
* `method`: the relevant method it written in the doc, for example `get`. | ||
* `path_name`: the paths it written in the api doc, for example `/pet`. | ||
* `method`: the relevant method it written in the api doc, for example `get`. | ||
* `parameters`: | ||
@@ -64,2 +67,6 @@ * `validate`: ajv validator that check: paths, files, queries and headers. | ||
* `errors`: in case of fail validation it return array of errors, otherwise return null | ||
* `responses`: contain array of statusCodes | ||
* `statusCode`: | ||
* `validate`: ajv validator that check body and headers. | ||
* `errors`: in case of fail validation it return array of errors, otherwise return null | ||
@@ -78,2 +85,4 @@ | ||
- `expectFormFieldsInBody` - Boolean that indicates whether form fields of non-file type that are specified in the schema should be validated against request body (e. g. Multer is copying text form fields to body) | ||
- `buildRequests` - Boolean that indicates whether if create validators for requests, default is true. | ||
- `buildResponses` - Boolean that indicates whether if create validators for responses, default is false. | ||
@@ -89,8 +98,10 @@ ```js | ||
## Usage Example | ||
### Validate request | ||
```js | ||
apiSchemaBuilder.getSchema('test/unit-tests/input-validation/pet-store-swagger.yaml') | ||
apiSchemaBuilder.buildSchema('test/unit-tests/input-validation/pet-store-swagger.yaml') | ||
.then(function (schema) { | ||
let schemaEndpoint = schema['/pet']['post']; | ||
//validate parameters | ||
//validate request's parameters | ||
let isParametersMatch = schemaEndpoint.parameters.validate({ query: {}, | ||
@@ -101,3 +112,3 @@ headers: { 'public-key': '1.0'},path: {},files: undefined }); | ||
//validate body | ||
//validate request's body | ||
let isBodysMatch =schemaEndpoint.body.validate({'bark': 111}); | ||
@@ -113,9 +124,34 @@ expect(schemaEndpoint.body.errors).to.be.eql([{ | ||
]) | ||
expect(isBodysMatch).to.be.false; | ||
}); | ||
expect(isBodysMatch).to.be.false; | ||
``` | ||
### Validate response | ||
```js | ||
apiSchemaBuilder.buildSchema('test/unit-tests/input-validation/pet-store-swagger.yaml') | ||
.then(function (schema) { | ||
let schemaEndpoint = schema['/pet']['post'].responses['201']; | ||
//validate response's body and headers | ||
let isValid = schemaEndpoint.validate({ | ||
body :{ id:11, 'name': 111}, | ||
headers:{'x-next': '321'} | ||
}) | ||
expect(schemaEndpoint.errors).to.be.eql([ | ||
{ | ||
'dataPath': '.body.name', | ||
'keyword': 'type', | ||
'message': 'should be string', | ||
'params': { | ||
'type': 'string' | ||
}, | ||
'schemaPath': '#/body/properties/name/type' | ||
}]) | ||
expect(isValid).to.be.false; | ||
}); | ||
``` | ||
## Important Notes | ||
- Objects - it is important to set any objects with the property `type: object` inside your swagger file, although it isn't a must in the Swagger (OpenAPI) spec in order to validate it accurately with [ajv](https://www.npmjs.com/package/ajv) it must be marked as `object` | ||
- Response validator - it support now only open api 2. | ||
@@ -122,0 +158,0 @@ ## Open api 3 - known issues |
166
src/index.js
@@ -12,8 +12,8 @@ 'use strict'; | ||
/** | ||
* Initialize the input validation middleware` | ||
* @param {string} swaggerPath - the path for the swagger file | ||
* @param {Object} options - options.formats to add formats to ajv, options.beautifyErrors, options.firstError, options.expectFormFieldsInBody, options.fileNameField (default is 'fieldname' - multer package), options.ajvConfigBody and options.ajvConfigParams for config object that will be passed for creation of Ajv instance used for validation of body and parameters appropriately | ||
*/ | ||
const DEFAULT_SETTINGS = { | ||
buildRequests: true, | ||
buildResponses: true | ||
}; | ||
function buildSchema(swaggerPath, options) { | ||
let updatedOptions = Object.assign({}, DEFAULT_SETTINGS, options); | ||
return Promise.all([ | ||
@@ -23,63 +23,31 @@ SwaggerParser.dereference(swaggerPath), | ||
]).then(function ([dereferenced, referenced]) { | ||
return buildValidations(referenced, dereferenced, options); | ||
return buildValidations(referenced, dereferenced, updatedOptions); | ||
}); | ||
} | ||
function buildValidations(referenced, dereferenced, options = {}) { | ||
const { makeOptionalAttributesNullable = false } = options; | ||
function buildValidations(referenced, dereferenced, options) { | ||
const { buildRequests, buildResponses } = options; | ||
const schemas = {}; | ||
Object.keys(dereferenced.paths).forEach(function (currentPath) { | ||
let pathParameters = dereferenced.paths[currentPath].parameters || []; | ||
let parsedPath = dereferenced.basePath && dereferenced.basePath !== '/' ? dereferenced.basePath.concat(currentPath.replace(/{/g, ':').replace(/}/g, '')) : currentPath.replace(/{/g, ':').replace(/}/g, ''); | ||
let parsedPath = dereferenced.basePath && dereferenced.basePath !== '/' | ||
? dereferenced.basePath.concat(currentPath.replace(/{/g, ':').replace(/}/g, '')) | ||
: currentPath.replace(/{/g, ':').replace(/}/g, ''); | ||
schemas[parsedPath] = {}; | ||
Object.keys(dereferenced.paths[currentPath]).filter(function (parameter) { return parameter !== 'parameters' }) | ||
Object.keys(dereferenced.paths[currentPath]) | ||
.filter(function (parameter) { return parameter !== 'parameters' }) | ||
.forEach(function (currentMethod) { | ||
schemas[parsedPath][currentMethod.toLowerCase()] = {}; | ||
const isOpenApi3 = dereferenced.openapi === '3.0.0'; | ||
const parameters = dereferenced.paths[currentPath][currentMethod].parameters || []; | ||
if (isOpenApi3) { | ||
schemas[parsedPath][currentMethod].body = oas3.buildBodyValidation(dereferenced, referenced, currentPath, currentMethod, options); | ||
} else { | ||
let bodySchema = options.expectFormFieldsInBody | ||
? parameters.filter(function (parameter) { return (parameter.in === 'body' || (parameter.in === 'formData' && parameter.type !== 'file')) }) | ||
: parameters.filter(function (parameter) { return parameter.in === 'body' }); | ||
if (makeOptionalAttributesNullable) { | ||
schemaPreprocessor.makeOptionalAttributesNullable(bodySchema); | ||
} | ||
if (bodySchema.length > 0) { | ||
const validatedBodySchema = oas2.getValidatedBodySchema(bodySchema); | ||
let bodySchemaReference = referenced.paths[currentPath][currentMethod].parameters.filter(function (parameter) { return parameter.in === 'body' })[0] || {}; | ||
let schemaReference = bodySchemaReference.schema; | ||
schemas[parsedPath][currentMethod].body = oas2.buildBodyValidation(validatedBodySchema, dereferenced.definitions, referenced, currentPath, currentMethod, parsedPath, options, schemaReference); | ||
} | ||
let parsedMethod = currentMethod.toLowerCase(); | ||
let requestValidator; | ||
if (buildRequests) { | ||
requestValidator = buildRequestValidator(referenced, dereferenced, currentPath, | ||
parsedPath, currentMethod, options); | ||
} | ||
// response validation | ||
schemas[parsedPath][currentMethod].responses = {}; | ||
let responses = dereferenced.paths[currentPath][currentMethod].responses || []; | ||
Object.keys(responses).forEach(statusCode => { | ||
if (statusCode !== 'default') { | ||
let responseDereferenceSchema = responses[statusCode].schema; | ||
let responseDereferenceHeaders = responses[statusCode].headers || []; | ||
let contentTypes = dereferenced.paths[currentPath][currentMethod].produces || dereferenced.paths[currentPath].produces || dereferenced.produces; | ||
let headersValidator = (responseDereferenceHeaders || contentTypes) ? buildHeadersValidation(responseDereferenceHeaders, contentTypes, options) : undefined; | ||
let responseValidator; | ||
if (buildResponses){ | ||
responseValidator = buildResponseValidator(referenced, dereferenced, currentPath, parsedPath, currentMethod, options); | ||
} | ||
let responseSchema = referenced.paths[currentPath][currentMethod].responses[statusCode].schema; | ||
let bodyValidator = responseSchema ? oas2.buildBodyValidation(responseDereferenceSchema, dereferenced.definitions, referenced, currentPath, currentMethod, parsedPath, options, responseSchema) : undefined; | ||
if (headersValidator || bodyValidator) { | ||
schemas[parsedPath][currentMethod].responses[statusCode] = new Validators.ResponseValidator({ body: bodyValidator, headers: headersValidator }); | ||
} | ||
} | ||
}); | ||
let localParameters = parameters.filter(function (parameter) { | ||
return parameter.in !== 'body'; | ||
}).concat(pathParameters); | ||
if (localParameters.length > 0 || options.contentTypeValidation) { | ||
schemas[parsedPath][currentMethod].parameters = buildParametersValidation(localParameters, | ||
dereferenced.paths[currentPath][currentMethod].consumes || dereferenced.paths[currentPath].consumes || dereferenced.consumes, options); | ||
} | ||
schemas[parsedPath][parsedMethod] = Object.assign({}, requestValidator, { responses: responseValidator }); | ||
}); | ||
@@ -90,2 +58,66 @@ }); | ||
function buildRequestValidator(referenced, dereferenced, currentPath, parsedPath, currentMethod, options){ | ||
let requestSchema = {}; | ||
let pathParameters = dereferenced.paths[currentPath].parameters || []; | ||
const isOpenApi3 = dereferenced.openapi === '3.0.0'; | ||
const parameters = dereferenced.paths[currentPath][currentMethod].parameters || []; | ||
if (isOpenApi3) { | ||
requestSchema.body = oas3.buildRequestBodyValidation(dereferenced, referenced, currentPath, currentMethod, options); | ||
} else { | ||
let bodySchema = options.expectFormFieldsInBody | ||
? parameters.filter(function (parameter) { | ||
return (parameter.in === 'body' || | ||
(parameter.in === 'formData' && parameter.type !== 'file')); | ||
}) | ||
: parameters.filter(function (parameter) { return parameter.in === 'body' }); | ||
options.makeOptionalAttributesNullable && schemaPreprocessor.makeOptionalAttributesNullable(bodySchema); | ||
if (bodySchema.length > 0) { | ||
const validatedBodySchema = oas2.getValidatedBodySchema(bodySchema); | ||
requestSchema.body = oas2.buildRequestBodyValidation(validatedBodySchema, dereferenced.definitions, referenced, | ||
currentPath, currentMethod, options); | ||
} | ||
} | ||
let localParameters = parameters.filter(function (parameter) { | ||
return parameter.in !== 'body'; | ||
}).concat(pathParameters); | ||
if (localParameters.length > 0 || options.contentTypeValidation) { | ||
requestSchema.parameters = buildParametersValidation(localParameters, | ||
dereferenced.paths[currentPath][currentMethod].consumes || dereferenced.paths[currentPath].consumes || dereferenced.consumes, options); | ||
} | ||
return requestSchema; | ||
} | ||
function buildResponseValidator(referenced, dereferenced, currentPath, parsedPath, currentMethod, options){ | ||
// support now only oas2 | ||
if (dereferenced.openapi === '3.0.0'){ return } | ||
const responsesSchema = {}; | ||
const responses = dereferenced.paths[currentPath][currentMethod].responses; | ||
if (responses) { | ||
Object.keys(responses).forEach(statusCode => { | ||
let responseDereferenceSchema = responses[statusCode].schema; | ||
let responseDereferenceHeaders = responses[statusCode].headers; | ||
let contentTypes = dereferenced.paths[currentPath][currentMethod].produces || dereferenced.paths[currentPath].produces || dereferenced.produces; | ||
let headersValidator = (responseDereferenceHeaders || contentTypes) ? buildHeadersValidation(responseDereferenceHeaders, contentTypes, options) : undefined; | ||
let bodyValidator = responseDereferenceSchema ? oas2.buildResponseBodyValidation(responseDereferenceSchema, | ||
dereferenced.definitions, referenced, currentPath, currentMethod, options, statusCode) : undefined; | ||
if (headersValidator || bodyValidator) { | ||
responsesSchema[statusCode] = new Validators.ResponseValidator({ | ||
body: bodyValidator, | ||
headers: headersValidator | ||
}); | ||
} | ||
}); | ||
} | ||
return responsesSchema; | ||
} | ||
function createContentTypeHeaders(validate, contentTypes) { | ||
@@ -170,3 +202,3 @@ if (!validate || !contentTypes) return; | ||
} | ||
}, this); | ||
}); | ||
@@ -178,3 +210,2 @@ ajvParametersSchema.properties.headers.content = createContentTypeHeaders(options.contentTypeValidation, contentTypes); | ||
// split to diff parsers if needed | ||
function buildHeadersValidation(headers, contentTypes, options) { | ||
@@ -194,15 +225,14 @@ const defaultAjvOptions = { | ||
properties: {}, | ||
required: [], | ||
additionalProperties: true | ||
}; | ||
Object.keys(headers).forEach(key => { | ||
let headerObj = Object.assign({}, headers[key]); | ||
const headerName = key.toLowerCase(); | ||
const headerRequired = headerObj.required; | ||
if (headerRequired) ajvHeadersSchema.required.push(key); | ||
delete headerObj.name; | ||
delete headerObj.required; | ||
ajvHeadersSchema.properties[headerName] = headerObj; | ||
}, this); | ||
if (headers) { | ||
Object.keys(headers).forEach(key => { | ||
let headerObj = Object.assign({}, headers[key]); | ||
const headerName = key.toLowerCase(); | ||
delete headerObj.name; | ||
delete headerObj.required; | ||
ajvHeadersSchema.properties[headerName] = headerObj; | ||
}); | ||
} | ||
@@ -209,0 +239,0 @@ ajvHeadersSchema.content = createContentTypeHeaders(options.contentTypeValidation, contentTypes); |
@@ -8,3 +8,4 @@ | ||
getValidatedBodySchema, | ||
buildBodyValidation | ||
buildResponseBodyValidation, | ||
buildRequestBodyValidation | ||
}; | ||
@@ -37,13 +38,19 @@ | ||
function buildBodyValidation(schema, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath, options = {}, schemaReference) { | ||
function buildAjvValidator(ajvConfigBody, formats, keywords){ | ||
const defaultAjvOptions = { | ||
allErrors: true | ||
}; | ||
const ajvOptions = Object.assign({}, defaultAjvOptions, options.ajvConfigBody); | ||
const ajvOptions = Object.assign({}, defaultAjvOptions, ajvConfigBody); | ||
let ajv = new Ajv(ajvOptions); | ||
ajvUtils.addCustomKeyword(ajv, options.formats, options.keywords); | ||
ajvUtils.addCustomKeyword(ajv, formats, keywords); | ||
return ajv; | ||
} | ||
function buildResponseBodyValidation(schema, swaggerDefinitions, originalSwagger, currentPath, currentMethod, options, statusCode) { | ||
let ajv = buildAjvValidator(options.ajvConfigBody, options.formats, options.keywords); | ||
if (schema.discriminator) { | ||
return buildInheritance(schema.discriminator, swaggerDefinitions, originalSwagger, currentPath, currentMethod, parsedPath, ajv, schemaReference); | ||
let schemaReference = originalSwagger.paths[currentPath][currentMethod].responses[statusCode].schema; | ||
return buildInheritance(schema.discriminator, swaggerDefinitions, originalSwagger, currentPath, currentMethod, ajv, schemaReference); | ||
} else { | ||
@@ -53,3 +60,14 @@ return new Validators.SimpleValidator(ajv.compile(schema)); | ||
} | ||
function buildInheritance(discriminator, dereferencedDefinitions, swagger, currentPath, currentMethod, parsedPath, ajv, schemaReference = {}) { | ||
function buildRequestBodyValidation(schema, swaggerDefinitions, originalSwagger, currentPath, currentMethod, options) { | ||
let ajv = buildAjvValidator(options.ajvConfigBody, options.formats, options.keywords); | ||
if (schema.discriminator) { | ||
let schemaReference = originalSwagger.paths[currentPath][currentMethod].parameters.filter(function (parameter) { return parameter.in === 'body' })[0].schema; | ||
return buildInheritance(schema.discriminator, swaggerDefinitions, originalSwagger, currentPath, currentMethod, ajv, schemaReference); | ||
} else { | ||
return new Validators.SimpleValidator(ajv.compile(schema)); | ||
} | ||
} | ||
function buildInheritance(discriminator, dereferencedDefinitions, swagger, currentPath, currentMethod, ajv, schemaReference = {}) { | ||
var inheritsObject = { | ||
@@ -56,0 +74,0 @@ inheritance: [] |
@@ -9,6 +9,6 @@ | ||
module.exports = { | ||
buildBodyValidation | ||
buildRequestBodyValidation | ||
}; | ||
function buildBodyValidation(dereferenced, originalSwagger, currentPath, currentMethod, options = {}) { | ||
function buildRequestBodyValidation(dereferenced, originalSwagger, currentPath, currentMethod, { ajvConfigBody, formats, keywords }) { | ||
if (!dereferenced.paths[currentPath][currentMethod].requestBody) { | ||
@@ -21,6 +21,6 @@ return; | ||
}; | ||
const ajvOptions = Object.assign({}, defaultAjvOptions, options.ajvConfigBody); | ||
const ajvOptions = Object.assign({}, defaultAjvOptions, ajvConfigBody); | ||
let ajv = new Ajv(ajvOptions); | ||
ajvUtils.addCustomKeyword(ajv, options.formats, options.keywords); | ||
ajvUtils.addCustomKeyword(ajv, formats, keywords); | ||
@@ -27,0 +27,0 @@ if (bodySchemaV3.discriminator) { |
@@ -15,7 +15,6 @@ | ||
if (bodySchema) { | ||
// let bodyToValidate = data.body || {} | ||
bodyValidationResult = bodySchema.validate(data.body); | ||
bodyValidationErrors = bodySchema.errors ? addErrorPrefix(bodySchema.errors, 'body') : []; | ||
} | ||
if (headersSchema && data.headers) { | ||
if (headersSchema) { | ||
headersValidationResult = headersSchema.validate(data.headers); | ||
@@ -27,5 +26,4 @@ headersValidationErrors = headersSchema.errors ? addErrorPrefix(headersSchema.errors, 'headers') : []; | ||
this.errors = errors.length === 0 ? null : errors; | ||
let result = bodyValidationResult && headersValidationResult; | ||
return result; | ||
return bodyValidationResult && headersValidationResult; | ||
} | ||
@@ -32,0 +30,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
49299
13
0
689
172
0