Skip to content

Commit

Permalink
feat: option to enable response body casting (#451)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdimascio authored Nov 9, 2020
1 parent a2dbfd2 commit f06a2d2
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 30 deletions.
43 changes: 23 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

[![GitHub stars](https://img.shields.io/github/stars/cdimascio/express-openapi-validator.svg?style=social&label=Star&maxAge=2592000)](https://github.com/cdimascio/express-openapi-validator/stargazers/) [![Twitter URL](https://img.shields.io/twitter/url/https/github.com/cdimascio/express-openapi-validator.svg?style=social)](https://twitter.com/intent/tweet?text=Check%20out%20express-openapi-validator%20by%20%40CarmineDiMascio%20https%3A%2F%2Fgit.luolix.top%2Fcdimascio%2Fexpress-openapi-validator%20%F0%9F%91%8D)

## Install
## Install

```shell
npm install express-openapi-validator
Expand Down Expand Up @@ -66,7 +66,6 @@ _**Important:** Ensure express is configured with all relevant body parsers. Bod

In v4.x.x, the validator is installed as standard connect middleware using `app.use(...) and/or router.use(...)` ([example](https://github.com/cdimascio/express-openapi-validator/blob/v4/README.md#Example-Express-API-Server)). This differs from the v3.x.x the installation which required the `install` method(s). The `install` methods no longer exist in v4.


## Usage (options)

See [Advanced Usage](#Advanced-Usage) options to:
Expand Down Expand Up @@ -107,12 +106,11 @@ app.use('/spec', express.static(spec));

// 4. Install the OpenApiValidator onto your express app
app.use(
OpenApiValidator.middleware({
apiSpec: './api.yaml',
validateResponses: true, // <-- to validate responses
// unknownFormats: ['my-format'] // <-- to provide custom formats
}
),
OpenApiValidator.middleware({
apiSpec: './api.yaml',
validateResponses: true, // <-- to validate responses
// unknownFormats: ['my-format'] // <-- to provide custom formats
}),
);

// 5. Define routes using Express
Expand Down Expand Up @@ -557,6 +555,12 @@ Determines whether the validator should validate responses. Also accepts respons

- `"failing"` - additional properties that fail schema validation are automatically removed from the response.

**coerceTypes:**

- `true` - coerce scalar data types.
- `false` - (**default**) do not coerce types. (almost always the desired behavior)
- `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema).

For example:

```javascript
Expand Down Expand Up @@ -593,9 +597,9 @@ Determines whether the validator should validate securities e.g. apikey, basic,
Defines a list of custome formats.

- `[{ ... }]` - array of custom format objects. Each object must have the following properties:
- name: string (required) - the format name
- validate: (v: any) => boolean (required) - the validation function
- type: 'string' | 'number' (optional) - the format's type
- name: string (required) - the format name
- validate: (v: any) => boolean (required) - the validation function
- type: 'string' | 'number' (optional) - the format's type

e.g.

Expand All @@ -605,23 +609,23 @@ formats: [
name: 'my-three-digit-format',
type: 'number',
// validate returns true the number has 3 digits, false otherwise
validate: v => /^\d{3}$/.test(v.toString())
validate: (v) => /^\d{3}$/.test(v.toString()),
},
{
name: 'my-three-letter-format',
type: 'string',
// validate returns true the string has 3 letters, false otherwise
validate: v => /^[A-Za-z]{3}$/.test(v)
validate: (v) => /^[A-Za-z]{3}$/.test(v),
},
]
];
```

Then use it in a spec e.g.

```yaml
my_property:
type: string
format: my-three-letter-format'
my_property:
type: string
format: my-three-letter-format'
```
### ▪️ validateFormats (optional)
Expand Down Expand Up @@ -758,11 +762,9 @@ $refParser: {

Determines whether the validator should coerce value types to match the those defined in the OpenAPI spec. This option applies **only** to path params, query strings, headers, and cookies. _It is **highly unlikley** that will want to disable this. As such this option is deprecated and will be removed in the next major version_


- `true` (**default**) - coerce scalar data types.
- `"array"` - in addition to coercions between scalar types, coerce scalar data to an array with one element and vice versa (as required by the schema).


## The Base URL

The validator will only validate requests, securities, and responses that are under
Expand Down Expand Up @@ -1040,7 +1042,7 @@ app.use(OpenApiValidator.middleware({

**A:** First, it's important to note that this behavior does not impact validation. The validator will validate against the type defined in your spec.

In order to modify the `req.params`, express requires that a param handler be registered e.g. `app.param(...)` or `router.param(...)`. Since `app` is available to middleware functions, the validator registers an `app.param` handler to coerce and modify the values of `req.params` to their declared types. Unfortunately, express does not provide a means to determine the current router from a middleware function, hence the validator is unable to register the same param handler on an express router. Ultimately, this means if your handler function is defined on `app`, the values of `req.params` will be coerced to their declared types. If your handler function is declare on an `express.Router`, the values of `req.params` values will be of type `string` (You must coerce them e.g. `parseInt(req.params.id)`).
In order to modify the `req.params`, express requires that a param handler be registered e.g. `app.param(...)` or `router.param(...)`. Since `app` is available to middleware functions, the validator registers an `app.param` handler to coerce and modify the values of `req.params` to their declared types. Unfortunately, express does not provide a means to determine the current router from a middleware function, hence the validator is unable to register the same param handler on an express router. Ultimately, this means if your handler function is defined on `app`, the values of `req.params` will be coerced to their declared types. If your handler function is declare on an `express.Router`, the values of `req.params` values will be of type `string` (You must coerce them e.g. `parseInt(req.params.id)`).

## Contributors ✨

Expand Down Expand Up @@ -1110,6 +1112,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
Expand Down
1 change: 1 addition & 0 deletions src/framework/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type ValidateRequestOpts = {

export type ValidateResponseOpts = {
removeAdditional?: 'failing' | boolean;
coerceTypes?: boolean | 'array';
};

export type ValidateSecurityOpts = {
Expand Down
8 changes: 4 additions & 4 deletions src/middlewares/openapi.request.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class RequestValidator {
this.apiDoc = apiDoc;
this.requestOpts.allowUnknownQueryParameters =
options.allowUnknownQueryParameters;
this.ajv = createRequestAjv(apiDoc, options);
this.ajv = createRequestAjv(apiDoc, { ...options, coerceTypes: true });
this.ajvBody = createRequestAjv(apiDoc, { ...options, coerceTypes: false });
}

Expand Down Expand Up @@ -141,12 +141,12 @@ export class RequestValidator {
: undefined;

const data = {
query: req.query ?? {},
query: req.query ?? {},
headers: req.headers,
params: req.params,
params: req.params,
cookies,
body: req.body,
}
};
const valid = validator.validatorGeneral(data);
const validBody = validator.validatorBody(data);

Expand Down
5 changes: 1 addition & 4 deletions src/middlewares/openapi.response.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,7 @@ export class ResponseValidator {

constructor(openApiSpec: OpenAPIV3.Document, options: ajv.Options = {}) {
this.spec = openApiSpec;
this.ajvBody = createResponseAjv(openApiSpec, {
...options,
coerceTypes: false,
});
this.ajvBody = createResponseAjv(openApiSpec, options);

(<any>mung).onError = (err, req, res, next) => {
return next(err);
Expand Down
16 changes: 14 additions & 2 deletions src/openapi.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export class OpenApiValidator {
this.validateOptions(options);
this.normalizeOptions(options);

if (options.coerceTypes == null) options.coerceTypes = true;
if (options.validateRequests == null) options.validateRequests = true;
if (options.validateResponses == null) options.validateResponses = false;
if (options.validateSecurity == null) options.validateSecurity = true;
Expand Down Expand Up @@ -71,6 +70,7 @@ export class OpenApiValidator {
if (options.validateResponses === true) {
options.validateResponses = {
removeAdditional: false,
coerceTypes: false,
};
}

Expand Down Expand Up @@ -313,7 +313,18 @@ export class OpenApiValidator {
'securityHandlers is not supported. Use validateSecurities.handlers instead.',
);
}

const coerceResponseTypes = options?.validateResponses?.['coerceTypes'];
if (options.coerceTypes != null && coerceResponseTypes != null) {
throw ono(
'coerceTypes and validateResponses.coerceTypes are mutually exclusive',
);
}

if (options.coerceTypes) {
if (options?.validateResponses) {
options.validateResponses['coerceTypes'] = true;
}
console.warn('coerceTypes is deprecated.');
}

Expand Down Expand Up @@ -364,12 +375,13 @@ class AjvOptions {
}

get response(): ajv.Options {
const { removeAdditional } = <ValidateResponseOpts>(
const { coerceTypes, removeAdditional } = <ValidateResponseOpts>(
this.options.validateResponses
);
return {
...this.baseOptions(),
useDefaults: false,
coerceTypes,
removeAdditional,
};
}
Expand Down
54 changes: 54 additions & 0 deletions test/response.validation.coerce.types.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as path from 'path';
import { expect } from 'chai';
import * as request from 'supertest';
import { createApp } from './common/app';

const apiSpecPath = path.join('test', 'resources', 'response.validation.yaml');

describe('response validation with type coercion', () => {
let app = null;

before(async () => {
// set up express app
app = await createApp(
{
apiSpec: apiSpecPath,
validateResponses: {
coerceTypes: true,
},
},
3005,
(app) => {
app
.get(`${app.basePath}/boolean`, (req, res) => {
return res.json(req.query.value);
})
.get(`${app.basePath}/object`, (req, res) => {
return res.json({
id: '1', // we expect this to type coerce to number
name: 'name',
tag: 'tag',
bought_at: null,
});
});
},
false,
);
});

after(() => {
app.server.close();
});

it('should be able to return `true` as the response body', async () =>
request(app)
.get(`${app.basePath}/boolean?value=true`)
.expect(200)
.then((r: any) => {
expect(r.body).to.equal(true);
}));
it('should coerce id from string to number', async () =>
request(app)
.get(`${app.basePath}/object`)
.expect(200));
});

0 comments on commit f06a2d2

Please sign in to comment.