Skip to content

Commit

Permalink
Merge 8e3248c into c601c4c
Browse files Browse the repository at this point in the history
  • Loading branch information
YaelGit authored Jan 30, 2019
2 parents c601c4c + 8e3248c commit ebb95ad
Show file tree
Hide file tree
Showing 5 changed files with 393 additions and 4 deletions.
40 changes: 37 additions & 3 deletions packages/rest/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {RestHttpErrors} from './rest-http-error';
import {ResolvedRoute} from './router';
import {OperationArgs, PathParameterValues, Request} from './types';
import {validateRequestBody} from './validation/request-body.validator';
import {validateRequestQuery} from './validation/request-query.validator';
const debug = debugFactory('loopback:rest:parser');

/**
Expand All @@ -38,11 +39,18 @@ export async function parseOperationArgs(
operationSpec,
request,
);

const query = await requestBodyParser.loadRequestBodyIfNeeded(
operationSpec,
request,
);

return buildOperationArguments(
operationSpec,
request,
pathParams,
body,
query,
route.schemas,
);
}
Expand All @@ -52,6 +60,7 @@ function buildOperationArguments(
request: Request,
pathParams: PathParameterValues,
body: RequestBody,
query: RequestBody,
globalSchemas: SchemasObject,
): OperationArgs {
let requestBodyIndex: number = -1;
Expand All @@ -67,6 +76,12 @@ function buildOperationArguments(

const paramArgs: OperationArgs = [];

let isQuery = false;
let paramName = '';
let paramSchema = {};
let queryValue = {};
let schemasValue = {};

for (const paramSpec of operationSpec.parameters || []) {
if (isReferenceObject(paramSpec)) {
// TODO(bajtos) implement $ref parameters
Expand All @@ -77,11 +92,30 @@ function buildOperationArguments(
const rawValue = getParamFromRequest(spec, request, pathParams);
const coercedValue = coerceParameter(rawValue, spec);
paramArgs.push(coercedValue);
}

debug('Validating request body - value %j', body);
validateRequestBody(body, operationSpec.requestBody, globalSchemas);
if (spec.in === 'query' && paramSpec.schema != null) {
isQuery = true;
paramName = paramSpec.name;
paramSchema = paramSpec.schema || [];
// tslint:disable-next-line:no-any
(<any>queryValue)[paramName] = coercedValue;
// tslint:disable-next-line:no-any
(<any>schemasValue)[paramName] = paramSchema;
}
}

//if query parameters from URL - send to query validation
if (isQuery) {
query.value = queryValue;
globalSchemas = {properties: schemasValue};
query.schema = globalSchemas;
validateRequestQuery(query, operationSpec.requestBody, globalSchemas);
}
//if body parameters - send to body validation
else {
debug('Validating request body - value %j', body);
validateRequestBody(body, operationSpec.requestBody, globalSchemas);
}
if (requestBodyIndex > -1) paramArgs.splice(requestBodyIndex, 0, body.value);
return paramArgs;
}
Expand Down
11 changes: 11 additions & 0 deletions packages/rest/src/rest-http-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ export namespace RestHttpErrors {
},
);
}
export const INVALID_REQUEST_QUERY_MESSAGE =
'The request query is invalid. See error object details property for more info.';

export function invalidRequestQuery(): HttpErrors.HttpError {
return Object.assign(
new HttpErrors.UnprocessableEntity(INVALID_REQUEST_QUERY_MESSAGE),
{
code: 'VALIDATION_FAILED',
},
);
}

/**
* An invalid request body error contains a `details` property as the machine-readable error.
Expand Down
150 changes: 150 additions & 0 deletions packages/rest/src/validation/request-query.validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import {
RequestBodyObject,
SchemaObject,
ReferenceObject,
SchemasObject,
} from '@loopback/openapi-v3-types';
import * as AJV from 'ajv';
import * as debugModule from 'debug';
import * as util from 'util';
import {HttpErrors, RestHttpErrors, RequestBody} from '..';
import * as _ from 'lodash';

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

export type RequestQueryValidationOptions = AJV.Options;

/**
* Check whether the request query is valid according to the provided OpenAPI schema.
* The JSON schema is generated from the OpenAPI schema which is typically defined
* by `@requestQuery()`.
* The validation leverages AJS schema validator.
* @param query The request query parsed from an HTTP request.
* @param requestQuerySpec The OpenAPI requestQuery specification defined in `@requestQuery()`.
* @param globalSchemas The referenced schemas generated from `OpenAPISpec.components.schemas`.
*/
export function validateRequestQuery(
query: RequestBody,
requestQuerySpec?: RequestBodyObject,
globalSchemas: SchemasObject = {},
options: RequestQueryValidationOptions = {},
) {
const required = requestQuerySpec && requestQuerySpec.required;

if (required && query.value == undefined) {
const err = Object.assign(
new HttpErrors.BadRequest('Request query is required'),
{
code: 'MISSING_REQUIRED_PARAMETER',
parameterName: 'request query',
},
);
throw err;
}

const schema = query.schema;
/* istanbul ignore if */
if (debug.enabled) {
debug('Request query schema: %j', util.inspect(schema, {depth: null}));
}
if (!schema) return;

options = Object.assign({coerceTypes: query.coercionRequired}, options);
validateValueAgainstSchema(query.value, schema, globalSchemas, options);
}

/**
* Convert an OpenAPI schema to the corresponding JSON schema.
* @param openapiSchema The OpenAPI schema to convert.
*/
function convertToJsonSchema(openapiSchema: SchemaObject) {
const jsonSchema = toJsonSchema(openapiSchema);
delete jsonSchema['$schema'];
/* istanbul ignore if */
if (debug.enabled) {
debug(
'Converted OpenAPI schema to JSON schema: %s',
util.inspect(jsonSchema, {depth: null}),
);
}
return jsonSchema;
}

/**
* Validate the request query data against JSON schema.
* @param query The request query data.
* @param schema The JSON schema used to perform the validation.
* @param globalSchemas Schema references.
*/

const compiledSchemaCache = new WeakMap();

function validateValueAgainstSchema(
// tslint:disable-next-line:no-any
query: any,
schema: SchemaObject | ReferenceObject,
globalSchemas?: SchemasObject,
options?: RequestQueryValidationOptions,
) {
let validate;

if (compiledSchemaCache.has(schema)) {
validate = compiledSchemaCache.get(schema);
} else {
validate = createValidator(schema, globalSchemas, options);
compiledSchemaCache.set(schema, validate);
}

if (validate(query)) {
debug('Request query passed AJV validation.');
return;
}

const validationErrors = validate.errors;

/* istanbul ignore if */
if (debug.enabled) {
debug(
'Invalid request query: %s. Errors: %s',
util.inspect(query, {depth: null}),
util.inspect(validationErrors),
);
}

const error = RestHttpErrors.invalidRequestQuery();
error.details = _.map(validationErrors, e => {
return {
path: e.dataPath,
code: e.keyword,
message: e.message,
info: e.params,
};
});
throw error;
}

function createValidator(
schema: SchemaObject,
globalSchemas?: SchemasObject,
options?: RequestQueryValidationOptions,
): Function {
const jsonSchema = convertToJsonSchema(schema);

const schemaWithRef = Object.assign({components: {}}, jsonSchema);
schemaWithRef.components = {
schemas: globalSchemas,
};

const ajv = new AJV(
Object.assign(
{},
{
allErrors: true,
},
options,
),
);

return ajv.compile(schemaWithRef);
}
2 changes: 1 addition & 1 deletion packages/rest/test/unit/coercion/paramObject.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ describe('coerce object param - optional', function() {
test(OPTIONAL_ANY_OBJECT, {key: 'value'}, {key: 'value'});
test(OPTIONAL_ANY_OBJECT, undefined, undefined);
test(OPTIONAL_ANY_OBJECT, '', undefined);
test(OPTIONAL_ANY_OBJECT, 'null', null);
test(OPTIONAL_ANY_OBJECT, {key: 'null'}, {key: 'null'});
});

context('nested values are not coerced', () => {
Expand Down
Loading

0 comments on commit ebb95ad

Please sign in to comment.