Skip to content

Commit

Permalink
refactor middleware registration
Browse files Browse the repository at this point in the history
  • Loading branch information
Carmine DiMascio committed Nov 2, 2019
1 parent 1edde3c commit 37944a3
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 64 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ or
Determines whether the validator should validate requests.

- `true` (**default**) - validate requests.
- `false` - do not validate requests.
- `false` (deprecated, to be removed in v3) - do not validate requests.

### ▪️ validateResponses (optional)

Expand Down
22 changes: 22 additions & 0 deletions src/framework/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ type OpenAPIErrorTransformer = ({}, {}) => object;

type PathSecurityTuple = [RegExp, SecurityRequirement[]];

export type SecurityHandlers = {
[key: string]: (
req: Request,
scopes: string[],
schema: OpenAPIV3.SecuritySchemeObject,
) => boolean | Promise<boolean>;
};

export type ValidateResponseOpts = {
removeAdditional?: string | boolean;
};

export interface OpenApiValidatorOpts {
apiSpec: OpenAPIV3.Document | string;
validateResponses?: boolean | ValidateResponseOpts;
validateRequests?: boolean;
securityHandlers?: SecurityHandlers;
coerceTypes?: boolean;
unknownFormats?: string[] | string | boolean;
multerOpts?: {};
}

interface SecurityRequirement {
[name: string]: SecurityScope[];
}
Expand Down
113 changes: 53 additions & 60 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,17 @@
import ono from 'ono';
import * as _ from 'lodash';
import {
Application,
Request,
Response,
NextFunction,
RequestHandler,
} from 'express';
import { Application, Response, NextFunction } from 'express';
import { OpenApiContext } from './framework/openapi.context';
import {
OpenAPIV3,
OpenApiValidatorOpts,
ValidateResponseOpts,
OpenApiRequest,
OpenApiRequestHandler,
} from './framework/types';
import * as middlewares from './middlewares';

export type SecurityHandlers = {
[key: string]: (
req: Request,
scopes: string[],
schema: OpenAPIV3.SecuritySchemeObject,
) => boolean | Promise<boolean>;
};
export type ValidateResponseOpts = {
removeAdditional?: string | boolean;
};
export interface OpenApiValidatorOpts {
apiSpec: OpenAPIV3.Document | string;
validateResponses?: boolean | ValidateResponseOpts;
validateRequests?: boolean;
securityHandlers?: SecurityHandlers;
coerceTypes?: boolean;
unknownFormats?: string[] | string | boolean;
multerOpts?: {};
}

export class OpenApiValidator {
private app: Application;
private context: OpenApiContext;
private options: OpenApiValidatorOpts;

Expand All @@ -46,28 +22,40 @@ export class OpenApiValidator {
if (options.coerceTypes == null) options.coerceTypes = true;
if (options.validateRequests == null) options.validateRequests = true;
if (options.validateResponses == null) options.validateResponses = false;
if (!options.validateRequests) throw Error('validateRequests must be true');

if (!options.validateResponses) {
} else if (
options.validateResponses === true ||
options.validateResponses === 'strict'
) {
if (options.validateResponses === true) {
options.validateResponses = {
removeAdditional: false,
};
}

this.options = options;

const openApiContext = new OpenApiContext({
this.context = new OpenApiContext({
apiDoc: options.apiSpec,
});

this.context = openApiContext;
}

install(app: Application) {
this.app = app;
this.installPathParams();
this.installMetadataMiddleware();
this.installMultipartMiddleware();

const components = this.context.apiDoc.components;
if (components && components.securitySchemes) {
this.installSecurityMiddleware();
}

if (this.options.validateRequests) {
this.installRequestValidationMiddleware();
}

if (this.options.validateResponses) {
this.installResponseValidationMiddleware();
}
}

private installPathParams() {
const pathParams = [];
for (const route of this.context.routes) {
if (route.pathParams.length > 0) {
Expand All @@ -77,7 +65,7 @@ export class OpenApiValidator {

// install param on routes with paths
for (const p of _.uniq(pathParams)) {
app.param(
this.app.param(
p,
(
req: OpenApiRequest,
Expand All @@ -94,7 +82,25 @@ export class OpenApiValidator {
},
);
}
}

private installMetadataMiddleware() {
this.app.use(middlewares.applyOpenApiMetadata(this.context));
}

private installMultipartMiddleware() {
this.app.use(middlewares.multipart(this.context, this.options.multerOpts));
}

private installSecurityMiddleware() {
const securityMiddleware = middlewares.security(
this.context,
this.options.securityHandlers,
);
this.app.use(securityMiddleware);
}

private installRequestValidationMiddleware() {
const { coerceTypes, unknownFormats } = this.options;
const requestValidator = new middlewares.RequestValidator(
this.context.apiDoc,
Expand All @@ -106,13 +112,15 @@ export class OpenApiValidator {
unknownFormats,
},
);

const requestValidatorMw: OpenApiRequestHandler = (req, res, next) =>
const requestValidationHandler: OpenApiRequestHandler = (req, res, next) =>
requestValidator.validate(req, res, next);

const removeAdditional =
this.options.validateResponses &&
(<ValidateResponseOpts>this.options.validateResponses).removeAdditional;
this.app.use(requestValidationHandler);
}

private installResponseValidationMiddleware() {
const { coerceTypes, unknownFormats, validateResponses } = this.options;
const { removeAdditional } = <ValidateResponseOpts>validateResponses;

const responseValidator = new middlewares.ResponseValidator(
this.context.apiDoc,
Expand All @@ -123,22 +131,7 @@ export class OpenApiValidator {
},
);

const securityMiddleware = middlewares.security(
this.context,
this.options.securityHandlers,
);

const components = this.context.apiDoc.components;
const use = [
middlewares.applyOpenApiMetadata(this.context),
middlewares.multipart(this.context, this.options.multerOpts),
];
// TODO validate security functions exist for each security key
if (components && components.securitySchemes) use.push(securityMiddleware);
if (this.options.validateRequests) use.push(requestValidatorMw);
if (this.options.validateResponses) use.push(responseValidator.validate());

app.use(use);
this.app.use(responseValidator.validate());
}

private validateOptions(options: OpenApiValidatorOpts): void {
Expand Down
11 changes: 8 additions & 3 deletions src/middlewares/openapi.security.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { SecurityHandlers } from '../index';
import { OpenAPIV3, OpenApiRequest } from '../framework/types';
import {
OpenAPIV3,
OpenApiRequest,
SecurityHandlers,
} from '../framework/types';
import { validationError } from './util';
import { OpenApiContext } from '../framework/openapi.context';

Expand Down Expand Up @@ -36,7 +39,9 @@ export function security(
if (!pathSchema) {
// add openapi metadata to make this case more clear
// its not obvious that missig schema means methodNotAllowed
return next(validationError(405, req.path, `${req.method} method not allowed`));
return next(
validationError(405, req.path, `${req.method} method not allowed`),
);
}

// use the local security object or fallbac to api doc's security or undefined
Expand Down

0 comments on commit 37944a3

Please sign in to comment.