Skip to content

Commit

Permalink
Add support for ajv-keywords
Browse files Browse the repository at this point in the history
  • Loading branch information
techntools committed Jul 15, 2024
1 parent 5491383 commit d8a7e63
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 4 deletions.
28 changes: 26 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ app.get('/:foo', oapi.path({
})
```

### `OpenApiMiddleware.validPath([definition])`
### `OpenApiMiddleware.validPath([definition [, pathOpts]])`

Registers a path with the OpenAPI document, also ensures incoming requests are valid against the schema. The path
`definition` is an [`OperationObject`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operationObject)
Expand All @@ -175,7 +175,7 @@ can be used in an express app and will call `next(err) if the incoming request i

The error is created with (`http-errors`)[https://www.npmjs.com/package/http-errors], and then is augmented with
information about the schema and validation errors. Validation uses (`avj`)[https://www.npmjs.com/package/ajv],
and `err.validationErrors` is the format exposed by that package.
and `err.validationErrors` is the format exposed by that package. Pass { keywords: [] } as pathOpts to support custom validation based on [ajv-keywords](https://www.npmjs.com/package/ajv-keywords).

**Example:**

Expand Down Expand Up @@ -215,6 +215,30 @@ app.get('/:foo', oapi.validPath({
schema: err.validationSchema
})
})

app.get('/zoom', oapi.validPath({
...
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
name: { type: 'string', not: { regexp: '/^[A-Z]/' } }
}
}
}
}
},
...
}, { keywords: ['regexp'] }), (err, req, res, next) => {
res.status(err.status).json({
error: err.message,
validation: err.validationErrors,
schema: err.validationSchema
})
})
```

### `OpenApiMiddleware.component(type[, name[, definition]])`
Expand Down
4 changes: 2 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ module.exports = function ExpressOpenApi (_routePrefix, _doc, _opts) {
}

// Validate path middleware
middleware.validPath = function (schema = {}) {
middleware.validPath = function (schema = {}, pathOpts = {}) {
let validate
function validSchemaMiddleware (req, res, next) {
if (!validate) {
validate = makeValidator(middleware, getSchema(validSchemaMiddleware), opts)
validate = makeValidator(middleware, getSchema(validSchemaMiddleware), { ...pathOpts, ...opts })
}
return validate(req, res, next)
}
Expand Down
3 changes: 3 additions & 0 deletions lib/validate.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict'
const Ajv = require('ajv')
const addFormats = require('ajv-formats')
const addKeywords = require('ajv-keywords')
const httpErrors = require('http-errors')
const merge = require('merge-deep')

Expand Down Expand Up @@ -93,6 +94,8 @@ module.exports = function makeValidatorMiddleware (middleware, schema, opts) {
strict: opts.strict === true ? opts.strict : false
})
addFormats(ajv)

if (opts.keywords) { addKeywords(ajv, opts.keywords) }
}

if (!validate) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"dependencies": {
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.1.0",
"http-errors": "^2.0.0",
"merge-deep": "^3.0.3",
"path-to-regexp": "^6.2.1",
Expand Down
108 changes: 108 additions & 0 deletions test/_validate.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,114 @@ module.exports = function () {

assert.strictEqual(res4.statusCode, 400)
assert.strictEqual(res4.body.validationErrors[0].instancePath, '/body/birthday')

app.put('/zoom', oapi.validPath({
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
name: { type: 'string', not: { regexp: '/^[A-Z]/' } }
}
}
}
}
},
responses: {
200: {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
goodbye: { type: 'string', enum: ['moon'] }
}
}
}
}
}
}
}, { keywords: ['regexp'] }), (req, res) => {
res.status(200).json({
goodbye: 'moon',
num: req.query.num
})
}, (err, req, res, next) => {
assert(err)
res.status(err.statusCode).json(err)
})

const res5 = await supertest(app)
.put('/zoom')
.send({
hello: 'world',
foo: 'bar',
name: 'abc'
})

assert.strictEqual(res5.statusCode, 200)

const res6 = await supertest(app)
.put('/zoom')
.send({
hello: 'world',
foo: 'bar',
name: 'Abc'
})

assert.strictEqual(res6.statusCode, 400)
assert.strictEqual(res6.body.validationErrors[0].instancePath, '/body/name')

app.get('/me', oapi.validPath({
parameters: [{
name: 'q',
in: 'query',
schema: {
type: 'string',
regexp: {
pattern: '^o',
flags: 'i'
}
}
}],
responses: {
200: {
description: 'Successful response',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
goodbye: { type: 'string', enum: ['moon'] }
}
}
}
}
}
}
}, { keywords: ['regexp'] }), (req, res) => {
res.status(200).json({
goodbye: 'moon',
})
}, (err, req, res, next) => {
assert(err)
res.status(err.statusCode).json(err)
})

const res7 = await supertest(app)
.get('/me?q=123')

assert.strictEqual(res7.statusCode, 400)
assert.strictEqual(res7.body.validationErrors[0].instancePath, '/query/q')

const res8 = await supertest(app)
.get('/me?q=oops')

assert.strictEqual(res8.statusCode, 200)
assert.strictEqual(res8.body.goodbye, 'moon')
})

test('coerce types on req', async function () {
Expand Down

0 comments on commit d8a7e63

Please sign in to comment.