From 847c8d97619c8feb3b1443ef1d5261c1c8b1eab2 Mon Sep 17 00:00:00 2001 From: Carmine DiMascio Date: Mon, 23 Dec 2019 10:52:52 -0500 Subject: [PATCH] factor parameters transform --- .../modded.express.mung.ts | 0 src/middlewares/openapi.request.validator.ts | 99 +++-------------- src/middlewares/openapi.response.validator.ts | 2 +- .../parameters.parse.ts} | 16 ++- .../parameters/parameters.transform.ts | 104 ++++++++++++++++++ tsconfig.json | 2 +- 6 files changed, 133 insertions(+), 90 deletions(-) rename src/{middlewares => framework}/modded.express.mung.ts (100%) rename src/middlewares/{parameters.ts => parameters/parameters.parse.ts} (92%) create mode 100644 src/middlewares/parameters/parameters.transform.ts diff --git a/src/middlewares/modded.express.mung.ts b/src/framework/modded.express.mung.ts similarity index 100% rename from src/middlewares/modded.express.mung.ts rename to src/framework/modded.express.mung.ts diff --git a/src/middlewares/openapi.request.validator.ts b/src/middlewares/openapi.request.validator.ts index d335ccad..35da57e8 100644 --- a/src/middlewares/openapi.request.validator.ts +++ b/src/middlewares/openapi.request.validator.ts @@ -16,7 +16,8 @@ import { OpenApiRequestMetadata, } from '../framework/types'; -import { Parameters } from './parameters'; +import { ParametersParser } from './parameters/parameters.parse'; +import { ParametersTransform } from './parameters/parameters.transform'; export class RequestValidator { private _middlewareCache: { [key: string]: RequestHandler } = {}; @@ -75,7 +76,7 @@ export class RequestValidator { pathSchema: OpenAPIV3.OperationObject, contentType: ContentType, ): RequestHandler { - const parser = new Parameters(this._apiDocs); + const parser = new ParametersParser(this._apiDocs); const parameters = parser.parse(path, pathSchema.parameters); const securityQueryParam = Security.queryParam(this._apiDocs, pathSchema); @@ -105,31 +106,10 @@ export class RequestValidator { const validator = this.ajv.compile(schema); return (req: OpenApiRequest, res: Response, next: NextFunction): void => { - // forcing convert to object if scheme describes param as object + explode - // for easy validation, keep the schema but update whereabouts of its sub components - parameters.parseObjectExplode.forEach(item => { - if (req[item.reqField]) { - // check if there is at least one of the nested properties before create the parent - const atLeastOne = item.properties.some(p => - req[item.reqField].hasOwnProperty(p), - ); - if (atLeastOne) { - req[item.reqField][item.name] = {}; - item.properties.forEach(property => { - if (req[item.reqField][property]) { - const type = - schema.properties[item.reqField].properties[item.name] - .properties?.[property]?.type; - const value = req[item.reqField][property]; - const coercedValue = - type === 'array' && !Array.isArray(value) ? [value] : value; - req[item.reqField][item.name][property] = coercedValue; - delete req[item.reqField][property]; - } - }); - } - } - }); + const parametersRequest = new ParametersTransform(parameters, schema); + + parametersRequest.applyExplodedJsonTransform(req); + parametersRequest.applyExplodedJsonArrayTransform(req); if (!this._requestOpts.allowUnknownQueryParameters) { this.rejectUnknownQueryParams( @@ -139,69 +119,18 @@ export class RequestValidator { ); } - const openapi = req.openapi; - const shouldUpdatePathParams = Object.keys(openapi.pathParams).length > 0; + parametersRequest.applyPathTransform(req); + parametersRequest.applyJsonTransform(req); + parametersRequest.applyJsonArrayTransform(req); - if (shouldUpdatePathParams) { - req.params = openapi.pathParams ?? req.params; - } - - /** - * support json in request params, query, headers and cookies - * like this filter={"type":"t-shirt","color":"blue"} - * - * https://swagger.io/docs/specification/describing-parameters/#schema-vs-content - */ - parameters.parseJson.forEach(item => { - if (req[item.reqField]?.[item.name]) { - try { - req[item.reqField][item.name] = JSON.parse( - req[item.reqField][item.name], - ); - } catch (e) { - // NOOP If parsing failed but _should_ contain JSON, validator will catch it. - // May contain falsely flagged parameter (e.g. input was object OR string) - } - } - }); - - /** - * array deserialization - * filter=foo,bar,baz - * filter=foo|bar|baz - * filter=foo%20bar%20baz - */ - parameters.parseArray.forEach(item => { - if (req[item.reqField]?.[item.name]) { - req[item.reqField][item.name] = req[item.reqField][item.name].split( - item.delimiter, - ); - } - }); - - /** - * forcing convert to array if scheme describes param as array + explode - */ - parameters.parseArrayExplode.forEach(item => { - if ( - req[item.reqField]?.[item.name] && - !(req[item.reqField][item.name] instanceof Array) - ) { - req[item.reqField][item.name] = [req[item.reqField][item.name]]; - } - }); + const cookies = req.cookies + ? { ...req.cookies, ...req.signedCookies } + : undefined; - const reqToValidate = { - ...req, - cookies: req.cookies - ? { ...req.cookies, ...req.signedCookies } - : undefined, - }; - const valid = validator(reqToValidate); + const valid = validator({ ...req, cookies }); if (valid) { next(); } else { - // TODO look into Ajv async errors plugins const errors = augmentAjvErrors([...(validator.errors ?? [])]); const err = ajvErrorsToValidatorError(400, errors); const message = this.ajv.errorsText(errors, { dataVar: 'request' }); diff --git a/src/middlewares/openapi.response.validator.ts b/src/middlewares/openapi.response.validator.ts index d1fd2e76..459d4d42 100644 --- a/src/middlewares/openapi.response.validator.ts +++ b/src/middlewares/openapi.response.validator.ts @@ -1,6 +1,6 @@ import ono from 'ono'; import * as ajv from 'ajv'; -import mung from './modded.express.mung'; +import mung from '../framework/modded.express.mung'; import { createResponseAjv } from '../framework/ajv'; import { augmentAjvErrors, diff --git a/src/middlewares/parameters.ts b/src/middlewares/parameters/parameters.parse.ts similarity index 92% rename from src/middlewares/parameters.ts rename to src/middlewares/parameters/parameters.parse.ts index c0b633ce..1f83dac8 100644 --- a/src/middlewares/parameters.ts +++ b/src/middlewares/parameters/parameters.parse.ts @@ -1,5 +1,5 @@ -import { OpenAPIV3 } from '../framework/types'; -import { validationError } from './util'; +import { OpenAPIV3 } from '../../framework/types'; +import { validationError } from '../util'; import * as mediaTypeParser from 'media-typer'; import * as contentTypeParser from 'content-type'; @@ -45,7 +45,11 @@ export interface ParametersParse { parseObjectExplode: ParseObjectExplode[]; } -export class Parameters { +/** + * A class top arse incoing parameters and populate a list of request fields e.g. id and field types e.g. query + * whose value must later be parsed as a JSON object, JSON Exploded Object, JSON Array, or JSON Exploded Array + */ +export class ParametersParser { private _apiDocs: OpenAPIV3.Document; private parseJson: ParseJson[] = []; private parseArray: ParseArray[] = []; @@ -56,6 +60,12 @@ export class Parameters { this._apiDocs = apiDocs; } + /** + * Parse incoing parameters and populate a list of request fields e.g. id and field types e.g. query + * whose value must later be parsed as a JSON object, JSON Exploded Object, JSON Array, or JSON Exploded Array + * @param path + * @param parameters + */ public parse(path: string, parameters: Parameter[] = []): ParametersParse { const schemas = { query: {}, headers: {}, params: {}, cookies: {} }; diff --git a/src/middlewares/parameters/parameters.transform.ts b/src/middlewares/parameters/parameters.transform.ts new file mode 100644 index 00000000..92fb7de7 --- /dev/null +++ b/src/middlewares/parameters/parameters.transform.ts @@ -0,0 +1,104 @@ +import { OpenApiRequest, OpenApiRequestMetadata } from '../../framework/types'; +import { ParametersParse } from './parameters.parse'; + +/** + * A class top arse incoing parameters and populate a list of request fields e.g. id and field types e.g. query + * whose value must later be parsed as a JSON object, JSON Exploded Object, JSON Array, or JSON Exploded Array + */ +export class ParametersTransform { + private parameters: ParametersParse; + private schema; + + constructor(parseResult: ParametersParse, schema) { + this.parameters = parseResult; + this.schema = schema; + } + + applyExplodedJsonTransform(req: OpenApiRequest) { + // forcing convert to object if scheme describes param as object + explode + // for easy validation, keep the schema but update whereabouts of its sub components + this.parameters.parseObjectExplode.forEach(item => { + if (req[item.reqField]) { + // check if there is at least one of the nested properties before create the parent + const atLeastOne = item.properties.some(p => + req[item.reqField].hasOwnProperty(p), + ); + if (atLeastOne) { + req[item.reqField][item.name] = {}; + item.properties.forEach(property => { + if (req[item.reqField][property]) { + const type = this.schema.properties[item.reqField].properties[ + item.name + ].properties?.[property]?.type; + const value = req[item.reqField][property]; + const coercedValue = + type === 'array' && !Array.isArray(value) ? [value] : value; + req[item.reqField][item.name][property] = coercedValue; + delete req[item.reqField][property]; + } + }); + } + } + }); + } + + applyJsonTransform(req: OpenApiRequest) { + /** + * support json in request params, query, headers and cookies + * like this filter={"type":"t-shirt","color":"blue"} + * + * https://swagger.io/docs/specification/describing-parameters/#schema-vs-content + */ + this.parameters.parseJson.forEach(item => { + if (req[item.reqField]?.[item.name]) { + try { + req[item.reqField][item.name] = JSON.parse( + req[item.reqField][item.name], + ); + } catch (e) { + // NOOP If parsing failed but _should_ contain JSON, validator will catch it. + // May contain falsely flagged parameter (e.g. input was object OR string) + } + } + }); + } + + applyJsonArrayTransform(req: OpenApiRequest) { + /** + * array deserialization + * filter=foo,bar,baz + * filter=foo|bar|baz + * filter=foo%20bar%20baz + */ + this.parameters.parseArray.forEach(item => { + if (req[item.reqField]?.[item.name]) { + req[item.reqField][item.name] = req[item.reqField][item.name].split( + item.delimiter, + ); + } + }); + } + + applyExplodedJsonArrayTransform(req: OpenApiRequest) { + /** + * forcing convert to array if scheme describes param as array + explode + */ + this.parameters.parseArrayExplode.forEach(item => { + if ( + req[item.reqField]?.[item.name] && + !(req[item.reqField][item.name] instanceof Array) + ) { + req[item.reqField][item.name] = [req[item.reqField][item.name]]; + } + }); + } + + applyPathTransform(req: OpenApiRequest) { + const openapi = req.openapi; + const shouldUpdatePathParams = Object.keys(openapi.pathParams).length > 0; + + if (shouldUpdatePathParams) { + req.params = openapi.pathParams ?? req.params; + } + } +} diff --git a/tsconfig.json b/tsconfig.json index 692a00de..0c529e7a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,6 @@ "include": [ "typings/**/*.d.ts", "src/**/*.ts", - "src/middlewares/modded.express.mung.ts" + "src/framework/modded.express.mung.ts" ] }