Skip to content

Commit acfb1ac

Browse files
committed
feat(rest): use RequestBody for validation
1 parent eb3af8f commit acfb1ac

File tree

3 files changed

+56
-54
lines changed

3 files changed

+56
-54
lines changed

packages/rest/src/parser.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
OperationObject,
1010
ParameterObject,
1111
ReferenceObject,
12+
SchemaObject,
1213
SchemasObject,
1314
} from '@loopback/openapi-v3-types';
1415
import * as debugModule from 'debug';
@@ -38,11 +39,11 @@ export const QUERY_NOT_PARSED = {};
3839
Object.freeze(QUERY_NOT_PARSED);
3940

4041
// tslint:disable:no-any
41-
type RequestBody = {
42+
export type RequestBody = {
4243
value: any | undefined;
4344
coercionRequired?: boolean;
4445
mediaType?: string;
45-
schema?: SchemasObject | ReferenceObject;
46+
schema?: SchemaObject | ReferenceObject;
4647
};
4748

4849
const parseJsonBody: (
@@ -222,9 +223,8 @@ function buildOperationArguments(
222223
}
223224

224225
debug('Validating request body - value %j', body);
225-
validateRequestBody(body.value, operationSpec.requestBody, globalSchemas, {
226+
validateRequestBody(body, operationSpec.requestBody, globalSchemas, {
226227
coerceTypes: body.coercionRequired,
227-
schema: body.schema,
228228
});
229229

230230
if (requestBodyIndex > -1) paramArgs.splice(requestBodyIndex, 0, body.value);

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

+23-34
Original file line numberDiff line numberDiff line change
@@ -12,36 +12,32 @@ import {
1212
import * as AJV from 'ajv';
1313
import * as debugModule from 'debug';
1414
import * as util from 'util';
15-
import {HttpErrors} from '..';
16-
import {RestHttpErrors} from '..';
15+
import {HttpErrors, RestHttpErrors, RequestBody} from '..';
1716
import * as _ from 'lodash';
1817

1918
const toJsonSchema = require('openapi-schema-to-json-schema');
2019
const debug = debugModule('loopback:rest:validation');
2120

22-
export interface RequestBodyValidationOptions extends AJV.Options {
23-
schema?: SchemaObject | ReferenceObject;
24-
}
21+
export type RequestBodyValidationOptions = AJV.Options;
2522

2623
/**
2724
* Check whether the request body is valid according to the provided OpenAPI schema.
2825
* The JSON schema is generated from the OpenAPI schema which is typically defined
2926
* by `@requestBody()`.
3027
* The validation leverages AJS schema validator.
31-
* @param body The body data from an HTTP request.
28+
* @param body The request body parsed from an HTTP request.
3229
* @param requestBodySpec The OpenAPI requestBody specification defined in `@requestBody()`.
3330
* @param globalSchemas The referenced schemas generated from `OpenAPISpec.components.schemas`.
3431
*/
3532
export function validateRequestBody(
36-
// tslint:disable-next-line:no-any
37-
body: any,
38-
requestBodySpec: RequestBodyObject | undefined,
39-
globalSchemas?: SchemasObject,
33+
body: RequestBody,
34+
requestBodySpec?: RequestBodyObject,
35+
globalSchemas: SchemasObject = {},
4036
options: RequestBodyValidationOptions = {},
4137
) {
42-
if (!requestBodySpec) return;
38+
const required = requestBodySpec && requestBodySpec.required;
4339

44-
if (requestBodySpec.required && body == undefined) {
40+
if (required && body.value == undefined) {
4541
const err = Object.assign(
4642
new HttpErrors.BadRequest('Request body is required'),
4743
{
@@ -52,24 +48,13 @@ export function validateRequestBody(
5248
throw err;
5349
}
5450

55-
const schema = options.schema || getRequestBodySchema(requestBodySpec);
56-
debug('Request body schema: %j', util.inspect(schema, {depth: null}));
51+
const schema = body.schema;
52+
if (debug.enabled) {
53+
debug('Request body schema: %j', util.inspect(schema, {depth: null}));
54+
}
5755
if (!schema) return;
5856

59-
validateValueAgainstSchema(body, schema, globalSchemas, options);
60-
}
61-
62-
/**
63-
* Get the schema from requestBody specification.
64-
* @param requestBodySpec The requestBody specification defined in `@requestBody()`.
65-
*/
66-
function getRequestBodySchema(
67-
requestBodySpec: RequestBodyObject,
68-
): SchemaObject | undefined {
69-
const content = requestBodySpec.content;
70-
// FIXME(bajtos) we need to find the entry matching the content-type
71-
// header from the incoming request (e.g. "application/json").
72-
return content[Object.keys(content)[0]].schema;
57+
validateValueAgainstSchema(body.value, schema, globalSchemas, options);
7358
}
7459

7560
/**
@@ -79,10 +64,12 @@ function getRequestBodySchema(
7964
function convertToJsonSchema(openapiSchema: SchemaObject) {
8065
const jsonSchema = toJsonSchema(openapiSchema);
8166
delete jsonSchema['$schema'];
82-
debug(
83-
'Converted OpenAPI schema to JSON schema: %s',
84-
util.inspect(jsonSchema, {depth: null}),
85-
);
67+
if (debug.enabled) {
68+
debug(
69+
'Converted OpenAPI schema to JSON schema: %s',
70+
util.inspect(jsonSchema, {depth: null}),
71+
);
72+
}
8673
return jsonSchema;
8774
}
8875

@@ -98,7 +85,7 @@ const compiledSchemaCache = new WeakMap();
9885
function validateValueAgainstSchema(
9986
// tslint:disable-next-line:no-any
10087
body: any,
101-
schema: SchemaObject,
88+
schema: SchemaObject | ReferenceObject,
10289
globalSchemas?: SchemasObject,
10390
options?: AJV.Options,
10491
) {
@@ -118,7 +105,9 @@ function validateValueAgainstSchema(
118105

119106
const validationErrors = validate.errors;
120107

121-
debug('Invalid request body: %s', util.inspect(validationErrors));
108+
if (debug.enabled) {
109+
debug('Invalid request body: %s', util.inspect(validationErrors));
110+
}
122111

123112
const error = RestHttpErrors.invalidRequestBody();
124113
error.details = _.map(validationErrors, e => {

packages/rest/test/unit/request-body.validator.test.ts

+29-16
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {validateRequestBody} from '../../src/validation/request-body.validator';
88
import {RestHttpErrors} from '../../';
99
import {aBodySpec} from '../helpers';
1010
import {
11-
RequestBodyObject,
11+
ReferenceObject,
1212
SchemaObject,
1313
SchemasObject,
1414
} from '@loopback/openapi-v3-types';
@@ -53,7 +53,10 @@ const INVALID_ACCOUNT_SCHEMA = {
5353

5454
describe('validateRequestBody', () => {
5555
it('accepts valid data omitting optional properties', () => {
56-
validateRequestBody({title: 'work'}, aBodySpec(TODO_SCHEMA));
56+
validateRequestBody(
57+
{value: {title: 'work'}, schema: TODO_SCHEMA},
58+
aBodySpec(TODO_SCHEMA),
59+
);
5760
});
5861

5962
it('rejects data missing a required property', () => {
@@ -72,7 +75,7 @@ describe('validateRequestBody', () => {
7275
{
7376
description: 'missing required "title"',
7477
},
75-
aBodySpec(TODO_SCHEMA),
78+
TODO_SCHEMA,
7679
);
7780
});
7881

@@ -93,7 +96,7 @@ describe('validateRequestBody', () => {
9396
title: 'todo with a string value of "isComplete"',
9497
isComplete: 'a string value',
9598
},
96-
aBodySpec(TODO_SCHEMA),
99+
TODO_SCHEMA,
97100
);
98101
});
99102

@@ -120,13 +123,13 @@ describe('validateRequestBody', () => {
120123
description: 'missing title and a string value of "isComplete"',
121124
isComplete: 'a string value',
122125
},
123-
aBodySpec(TODO_SCHEMA),
126+
TODO_SCHEMA,
124127
);
125128
});
126129

127130
it('reports schema generation errors', () => {
128131
expect(() =>
129-
validateRequestBody({}, aBodySpec(INVALID_ACCOUNT_SCHEMA)),
132+
validateRequestBody({value: {}, schema: INVALID_ACCOUNT_SCHEMA}),
130133
).to.throw(
131134
"can't resolve reference #/components/schemas/Invalid from id #",
132135
);
@@ -146,7 +149,7 @@ describe('validateRequestBody', () => {
146149
'VALIDATION_FAILED',
147150
details,
148151
{description: 'missing title'},
149-
aBodySpec({$ref: '#/components/schemas/Todo'}),
152+
{$ref: '#/components/schemas/Todo'},
150153
{Todo: TODO_SCHEMA},
151154
);
152155
});
@@ -157,12 +160,17 @@ describe('validateRequestBody', () => {
157160
'MISSING_REQUIRED_PARAMETER',
158161
undefined,
159162
null,
160-
aBodySpec(TODO_SCHEMA, {required: true}),
163+
TODO_SCHEMA,
164+
{},
165+
true,
161166
);
162167
});
163168

164169
it('allows empty values when body is optional', () => {
165-
validateRequestBody(null, aBodySpec(TODO_SCHEMA, {required: false}));
170+
validateRequestBody(
171+
{value: null, schema: TODO_SCHEMA},
172+
aBodySpec(TODO_SCHEMA, {required: false}),
173+
);
166174
});
167175

168176
it('rejects invalid values for number properties', () => {
@@ -184,7 +192,7 @@ describe('validateRequestBody', () => {
184192
'VALIDATION_FAILED',
185193
details,
186194
{count: 'string value'},
187-
aBodySpec(schema),
195+
schema,
188196
);
189197
});
190198

@@ -214,7 +222,7 @@ describe('validateRequestBody', () => {
214222
'VALIDATION_FAILED',
215223
details,
216224
{orders: ['order1', 1]},
217-
aBodySpec(schema),
225+
schema,
218226
);
219227
});
220228

@@ -238,7 +246,7 @@ describe('validateRequestBody', () => {
238246
'VALIDATION_FAILED',
239247
details,
240248
[{title: 'a good todo'}, {description: 'a todo item missing title'}],
241-
aBodySpec(schema),
249+
schema,
242250
{Todo: TODO_SCHEMA},
243251
);
244252
});
@@ -280,7 +288,7 @@ describe('validateRequestBody', () => {
280288
{description: 'a todo with wrong type of title', title: 2},
281289
],
282290
},
283-
aBodySpec(schema),
291+
schema,
284292
{Todo: TODO_SCHEMA},
285293
);
286294
});
@@ -314,7 +322,7 @@ describe('validateRequestBody', () => {
314322
{title: 'an account with invalid address', address: {city: 1}},
315323
],
316324
},
317-
aBodySpec(schema),
325+
schema,
318326
{Account: ACCOUNT_SCHEMA, Address: ADDRESS_SCHEMA},
319327
);
320328
});
@@ -328,11 +336,16 @@ function verifyValidationRejectsInputWithError(
328336
expectedCode: string,
329337
expectedDetails: RestHttpErrors.ValidationErrorDetails[] | undefined,
330338
body: object | null,
331-
spec: RequestBodyObject | undefined,
339+
schema: SchemaObject | ReferenceObject,
332340
schemas?: SchemasObject,
341+
required?: boolean,
333342
) {
334343
try {
335-
validateRequestBody(body, spec, schemas);
344+
validateRequestBody(
345+
{value: body, schema},
346+
aBodySpec(schema, {required}),
347+
schemas,
348+
);
336349
throw new Error(
337350
"expected Function { name: 'validateRequestBody' } to throw exception",
338351
);

0 commit comments

Comments
 (0)