@wesleytodd/openapi
Version:
Middleware for generating OpenAPI/Swagger documentation for your Express app
134 lines (119 loc) • 3.73 kB
JavaScript
const Ajv = require('ajv')
const addFormats = require('ajv-formats')
const addKeywords = require('ajv-keywords')
const httpErrors = require('http-errors')
const merge = require('merge-deep')
const BASE_REQ_SCHEMA = {
type: 'object',
required: ['headers', 'params', 'query'],
properties: {
headers: {
type: 'object',
required: [],
properties: {}
},
params: {
type: 'object',
required: [],
properties: {}
},
query: {
type: 'object',
required: [],
properties: {}
},
body: {
type: 'object',
required: [],
properties: {}
}
}
}
module.exports = function makeValidatorMiddleware (middleware, schema, opts) {
let ajv
let validate
function makeValidator () {
const reqSchema = merge({}, BASE_REQ_SCHEMA)
// Compile req schema on first request
// Build param validation
schema.parameters && schema.parameters.forEach((p) => {
switch (p.in) {
case 'path':
reqSchema.properties.params.properties[p.name] = p.schema
p.required && !reqSchema.properties.params.required.includes(p.name) && reqSchema.properties.params.required.push(p.name)
break
case 'query':
reqSchema.properties.query.properties[p.name] = p.schema
p.required && !reqSchema.properties.query.required.includes(p.name) && reqSchema.properties.query.required.push(p.name)
break
case 'header': {
const name = p.name.toLowerCase()
reqSchema.properties.headers.properties[name] = p.schema
p.required && !reqSchema.properties.headers.required.includes(p.name) && reqSchema.properties.headers.required.push(name)
break
}
}
})
// Compile req body schema
schema.requestBody && Object.entries(schema.requestBody.content)
.forEach(([contentType, { schema }]) => {
switch (contentType) {
case 'application/json':
reqSchema.properties.body = schema
break
default:
throw new TypeError(`Validation of content type not supported: ${contentType}`)
}
})
// Add components for references
reqSchema.components = middleware.document && middleware.document.components
return ajv.compile(reqSchema)
}
return function validateMiddleware (req, res, next) {
// Restrict validation to only "route" layers
// This prevents running any validation
// if we are in a .use call which could
// be a non-routable request thus
if (!req.route) {
return next()
}
// Create ajv instance on first request
if (!ajv) {
ajv = new Ajv({
coerceTypes: opts.coerce === 'false' ? opts.coerce : true,
strict: opts.strict === true ? opts.strict : false
})
addFormats(ajv)
if (opts.keywords) { addKeywords(ajv, opts.keywords) }
}
if (!validate) {
validate = makeValidator()
}
// Validate request
let r = req
if (opts.coerce !== true) {
r = makeReqCopy(req)
}
const validationStatus = validate(r)
if (validationStatus === true) {
return next()
}
// build error?
const err = new Error('Request validation failed')
err.validationErrors = validate.errors
err.validationSchema = validate.schema
next(httpErrors(400, err))
}
}
// This is because ajv modifies the original data,
// preventing this requires that we dont pass the
// actual req. An issue has been opened (@TODO open the issue)
function makeReqCopy (req) {
return JSON.parse(JSON.stringify({
headers: req.headers,
params: req.params,
query: req.query,
body: req.body
}))
}