Skip to content

Commit

Permalink
factor parameters transform
Browse files Browse the repository at this point in the history
  • Loading branch information
Carmine DiMascio committed Dec 23, 2019
1 parent a81b6c8 commit 847c8d9
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 90 deletions.
File renamed without changes.
99 changes: 14 additions & 85 deletions src/middlewares/openapi.request.validator.ts
Expand Up @@ -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 } = {};
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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(
Expand All @@ -139,69 +119,18 @@ export class RequestValidator {
);
}

const openapi = <OpenApiRequestMetadata>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' });
Expand Down
2 changes: 1 addition & 1 deletion 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,
Expand Down
@@ -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';

Expand Down Expand Up @@ -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[] = [];
Expand All @@ -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: {} };

Expand Down
104 changes: 104 additions & 0 deletions 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 = <OpenApiRequestMetadata>req.openapi;
const shouldUpdatePathParams = Object.keys(openapi.pathParams).length > 0;

if (shouldUpdatePathParams) {
req.params = openapi.pathParams ?? req.params;
}
}
}
2 changes: 1 addition & 1 deletion tsconfig.json
Expand Up @@ -13,6 +13,6 @@
"include": [
"typings/**/*.d.ts",
"src/**/*.ts",
"src/middlewares/modded.express.mung.ts"
"src/framework/modded.express.mung.ts"
]
}

0 comments on commit 847c8d9

Please sign in to comment.