Skip to content

Commit dfd282e

Browse files
authored
Merge pull request #87 from cdimascio/enums
enum validation errors should include a list of allowed enum values
2 parents 693c2f1 + 80274ec commit dfd282e

File tree

4 files changed

+48
-11
lines changed

4 files changed

+48
-11
lines changed

src/middlewares/openapi.request.validator.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
extractContentType,
44
validationError,
55
ajvErrorsToValidatorError,
6+
augmentAjvErrors,
67
} from './util';
78
import ono from 'ono';
89
import { NextFunction, Response } from 'express';
@@ -22,7 +23,11 @@ export class RequestValidator {
2223
this.ajv = createRequestAjv(apiDocs, options);
2324
}
2425

25-
public validate(req: OpenApiRequest, res: Response, next: NextFunction): void {
26+
public validate(
27+
req: OpenApiRequest,
28+
res: Response,
29+
next: NextFunction,
30+
): void {
2631
if (!req.openapi) {
2732
// this path was not found in open api and
2833
// this path is not defined under an openapi base path
@@ -138,12 +143,11 @@ export class RequestValidator {
138143
: undefined,
139144
};
140145
const valid = validator(reqToValidate);
141-
// save errors, Ajv overwrites errors on each validation call (race condition?)
142-
// TODO look into Ajv async errors plugins
143-
const errors = [...(validator.errors || [])];
144146
if (valid) {
145147
next();
146148
} else {
149+
// TODO look into Ajv async errors plugins
150+
const errors = augmentAjvErrors([...(validator.errors || [])]);
147151
const err = ajvErrorsToValidatorError(400, errors);
148152
const message = this.ajv.errorsText(errors, { dataVar: 'request' });
149153
throw ono(err, message);
@@ -166,8 +170,6 @@ export class RequestValidator {
166170
}
167171
}
168172

169-
170-
171173
private requestBodyToSchema(path, contentType, requestBody: any = {}) {
172174
if (requestBody.content) {
173175
const content = requestBody.content[contentType];

src/middlewares/openapi.response.validator.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as Ajv from 'ajv';
33
import mung from './modded.express.mung';
44
import { createResponseAjv } from './ajv';
55
import {
6+
augmentAjvErrors,
67
extractContentType,
78
ajvErrorsToValidatorError,
89
validationError,
@@ -84,7 +85,7 @@ export class ResponseValidator {
8485
});
8586

8687
if (!valid) {
87-
const errors = validator.errors;
88+
const errors = augmentAjvErrors(validator.errors);
8889
const message = this.ajv.errorsText(errors, {
8990
dataVar: '', // responses
9091
});

src/middlewares/util.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,23 @@ export function validationError(
3838
return ono(err, message);
3939
}
4040

41+
/**
42+
* (side-effecting) modifies the errors object
43+
* TODO - do this some other way
44+
* @param errors
45+
*/
46+
export function augmentAjvErrors(errors: Ajv.ErrorObject[] = []): Ajv.ErrorObject[] {
47+
errors.forEach(e => {
48+
if (e.keyword === 'enum') {
49+
const params: any = e.params;
50+
const allowedEnumValues = params && params.allowedValues;
51+
e.message = !!allowedEnumValues
52+
? `${e.message}: ${allowedEnumValues.join(', ')}`
53+
: e.message;
54+
}
55+
});
56+
return errors;
57+
}
4158
export function ajvErrorsToValidatorError(
4259
status: number,
4360
errors: Ajv.ErrorObject[],
@@ -51,7 +68,7 @@ export function ajvErrorsToValidatorError(
5168
params.missingProperty &&
5269
e.dataPath + '.' + params.missingProperty;
5370
const additionalProperty =
54-
e.params &&
71+
params &&
5572
params.additionalProperty &&
5673
e.dataPath + '.' + params.additionalProperty;
5774
const path = required || additionalProperty || e.dataPath || e.schemaPath;

test/routes.spec.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ describe(packageJson.name, () => {
102102
expect(e).to.have.length(1);
103103
expect(e[0].path).to.contain('testJson');
104104
expect(e[0].message).to.equal(
105-
'should be equal to one of the allowed values',
105+
'should be equal to one of the allowed values: bar, baz',
106106
);
107107
}));
108108

@@ -126,7 +126,7 @@ describe(packageJson.name, () => {
126126
expect(e).to.have.length(1);
127127
expect(e[0].path).to.contain('testArray');
128128
expect(e[0].message).to.equal(
129-
'should be equal to one of the allowed values',
129+
'should be equal to one of the allowed values: foo, bar, baz',
130130
);
131131
}));
132132

@@ -152,7 +152,7 @@ describe(packageJson.name, () => {
152152
expect(e).to.have.length(1);
153153
expect(e[0].path).to.contain('testArrayExplode');
154154
expect(e[0].message).to.equal(
155-
'should be equal to one of the allowed values',
155+
'should be equal to one of the allowed values: foo, bar, baz',
156156
);
157157
}));
158158
});
@@ -316,6 +316,23 @@ describe(packageJson.name, () => {
316316
});
317317
});
318318

319+
it('should return 400 an invalid enum value is given', async () => {
320+
return request(apps[i])
321+
.get(`${basePath}/pets`)
322+
.query({
323+
limit: 10,
324+
test: 'one',
325+
testArray: ['unknown_value'],
326+
})
327+
.expect(400)
328+
.then(r => {
329+
const e = r.body.errors;
330+
expect(e[0].message).equals(
331+
'should be equal to one of the allowed values: foo, bar, baz',
332+
);
333+
});
334+
});
335+
319336
it('should handle multiple path params with coereion', async () => {
320337
const id = '10';
321338
const attributeId = '12';

0 commit comments

Comments
 (0)