-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(rest): implement query parameter validation (issue 1573) #2307
Changes from 3 commits
72594eb
0e04254
8e3248c
a4c2086
066d525
4c6b176
f946d48
0121c10
181e1f1
0710055
91a37dc
a624b95
0e92b88
edbbe88
4695e3a
857868e
bfe8c27
a8a409c
84c6a88
1a6ac91
89340b0
d3a3bea
51cba45
a3da024
a54fbf1
bf26cc3
75731f9
5042698
b5f12be
6ef5d85
65ee865
596a143
95e919e
112847b
f14bd93
7707fa5
bf22e5c
131ccd9
1fe7f9c
14d7909
c2c1c07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'); | ||
|
||
/** | ||
|
@@ -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, | ||
); | ||
} | ||
|
@@ -52,6 +60,7 @@ function buildOperationArguments( | |
request: Request, | ||
pathParams: PathParameterValues, | ||
body: RequestBody, | ||
query: RequestBody, | ||
globalSchemas: SchemasObject, | ||
): OperationArgs { | ||
let requestBodyIndex: number = -1; | ||
|
@@ -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 | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIUC, you are building an object with all parameters from the query string, and then validate this object in one pass. I have two reservations about this approach:
Have you considered to call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hello @bajtos ,
|
||
} | ||
//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; | ||
} | ||
|
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we want to reuse There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @raymondfeng - do you mean we should duplicate the type requestBody in types.ts and then use this new type (ValueWithSchema) in request-query.validator.ts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No duplication. What I meant is to refactor |
||
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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be nice to see which There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. |
||
}, | ||
); | ||
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); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There seem to be quite a bit duplicate code as request-body-validator. Can we refactor the code for better reuse? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @raymondfeng -Thanks for the review and comments. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would create a utility function that validates |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is suspicious. Why do we parse the request body again into
query
?