Skip to content

Commit f7bb80d

Browse files
committed
feat(rest): add support for ajv-keywords
1 parent 28a049d commit f7bb80d

File tree

5 files changed

+115
-1
lines changed

5 files changed

+115
-1
lines changed

packages/rest/package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/rest/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@types/serve-static": "1.13.2",
3333
"@types/type-is": "^1.6.2",
3434
"ajv": "^6.10.2",
35+
"ajv-keywords": "^3.4.1",
3536
"body-parser": "^1.19.0",
3637
"cors": "^2.8.5",
3738
"debug": "^4.1.1",

packages/rest/src/__tests__/acceptance/validation/validation.acceptance.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ describe('Validation at REST level', () => {
3838
@property({required: false, type: 'string', jsonSchema: {nullable: true}})
3939
description?: string | null;
4040

41-
@property({required: true})
41+
@property({required: true, jsonSchema: {range: [0, 100]}})
4242
price: number;
4343

4444
constructor(data: Partial<Product>) {
@@ -115,6 +115,7 @@ describe('Validation at REST level', () => {
115115
givenAnAppAndAClient(ProductController, {
116116
nullable: false,
117117
compiledSchemaCache: new WeakMap(),
118+
ajvKeywords: ['range'],
118119
}),
119120
);
120121
after(() => app.stop());
@@ -150,6 +151,99 @@ describe('Validation at REST level', () => {
150151
},
151152
});
152153
});
154+
155+
it('rejects requests with price out of range', async () => {
156+
const DATA = {
157+
name: 'iPhone',
158+
description: 'iPhone',
159+
price: 200,
160+
};
161+
const res = await client
162+
.post('/products')
163+
.send(DATA)
164+
.expect(422);
165+
166+
expect(res.body).to.eql({
167+
error: {
168+
statusCode: 422,
169+
name: 'UnprocessableEntityError',
170+
message:
171+
'The request body is invalid. See error object `details` property for more info.',
172+
code: 'VALIDATION_FAILED',
173+
details: [
174+
{
175+
path: '.price',
176+
code: 'maximum',
177+
message: 'should be <= 100',
178+
info: {comparison: '<=', limit: 100, exclusive: false},
179+
},
180+
{
181+
path: '.price',
182+
code: 'range',
183+
message: 'should pass "range" keyword validation',
184+
info: {keyword: 'range'},
185+
},
186+
],
187+
},
188+
});
189+
});
190+
});
191+
192+
context('with request body validation options - {ajvKeywords: true}', () => {
193+
class ProductController {
194+
@post('/products')
195+
async create(
196+
@requestBody({required: true}) data: Product,
197+
): Promise<Product> {
198+
return new Product(data);
199+
}
200+
}
201+
202+
before(() =>
203+
givenAnAppAndAClient(ProductController, {
204+
nullable: false,
205+
compiledSchemaCache: new WeakMap(),
206+
$data: true,
207+
ajvKeywords: true,
208+
}),
209+
);
210+
after(() => app.stop());
211+
212+
it('rejects requests with price out of range', async () => {
213+
const DATA = {
214+
name: 'iPhone',
215+
description: 'iPhone',
216+
price: 200,
217+
};
218+
const res = await client
219+
.post('/products')
220+
.send(DATA)
221+
.expect(422);
222+
223+
expect(res.body).to.eql({
224+
error: {
225+
statusCode: 422,
226+
name: 'UnprocessableEntityError',
227+
message:
228+
'The request body is invalid. See error object `details` property for more info.',
229+
code: 'VALIDATION_FAILED',
230+
details: [
231+
{
232+
path: '.price',
233+
code: 'maximum',
234+
message: 'should be <= 100',
235+
info: {comparison: '<=', limit: 100, exclusive: false},
236+
},
237+
{
238+
path: '.price',
239+
code: 'range',
240+
message: 'should pass "range" keyword validation',
241+
info: {keyword: 'range'},
242+
},
243+
],
244+
},
245+
});
246+
});
153247
});
154248

155249
// A request body schema can be provided explicitly by the user

packages/rest/src/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ export interface RequestBodyValidationOptions extends ajv.Options {
101101
* to skip the default cache.
102102
*/
103103
compiledSchemaCache?: SchemaValidatorCache;
104+
/**
105+
* Enable additional AJV keywords from https://github.com/epoberezkin/ajv-keywords
106+
* - `true`: Add all keywords from `ajv-keywords`
107+
* - `string[]`: Add an array of keywords from `ajv-keywords`
108+
*/
109+
ajvKeywords?: true | string[];
104110
}
105111

106112
/* eslint-disable @typescript-eslint/no-explicit-any */

packages/rest/src/validation/request-body.validator.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {RequestBodyValidationOptions, SchemaValidatorCache} from '../types';
1919
const toJsonSchema = require('openapi-schema-to-json-schema');
2020
const debug = debugModule('loopback:rest:validation');
2121

22+
const ajvKeywords = require('ajv-keywords');
23+
2224
/**
2325
* Check whether the request body is valid according to the provided OpenAPI schema.
2426
* The JSON schema is generated from the OpenAPI schema which is typically defined
@@ -183,5 +185,11 @@ function createValidator(
183185
debug('AJV options', options);
184186
const ajv = new AJV(options);
185187

188+
if (options.ajvKeywords === true) {
189+
ajvKeywords(ajv);
190+
} else if (Array.isArray(options.ajvKeywords)) {
191+
ajvKeywords(ajv, options.ajvKeywords);
192+
}
193+
186194
return ajv.compile(schemaWithRef);
187195
}

0 commit comments

Comments
 (0)