Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/framework/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ export interface OpenApiValidatorOpts {
};
operationHandlers?: false | string | OperationHandlerOptions;
validateFormats?: boolean | 'fast' | 'full';
beforeRequestBodyValidation?: BeforeRequestBodyValidationHandlers;
afterResponseBodyValidation?: AfterResponseBodyValidationHandlers;
}

export interface NormalizedOpenApiValidatorOpts extends OpenApiValidatorOpts {
Expand Down Expand Up @@ -564,6 +566,25 @@ export interface OpenApiRequest extends Request {
openapi?: OpenApiRequestMetadata;
}

export type BeforeRequestBodyValidationHandler = (
req: OpenApiRequest,
schema: OpenAPIV3.OperationObject,
) => void | Promise<void>;

export type BeforeRequestBodyValidationHandlers = {
[handlerName: string]: BeforeRequestBodyValidationHandler;
};

export type AfterResponseBodyValidationHandler = (
body: any,
req: OpenApiRequest,
schema: OpenAPIV3.OperationObject,
) => any | Promise<any>;

export type AfterResponseBodyValidationHandlers = {
[handlerName: string]: AfterResponseBodyValidationHandler;
};

export type OpenApiRequestHandler = (
req: OpenApiRequest,
res: Response,
Expand Down
67 changes: 66 additions & 1 deletion src/middlewares/openapi.response.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
OpenApiRequestMetadata,
InternalServerError,
ValidateResponseOpts,
AfterResponseBodyValidationHandlers,
} from '../framework/types';
import * as mediaTypeParser from 'media-typer';
import * as contentTypeParser from 'content-type';
Expand All @@ -33,17 +34,20 @@ export class ResponseValidator {
} = {};
private eovOptions: ValidateResponseOpts;
private serial: number;
private afterResponseBodyValidationHandlers?: AfterResponseBodyValidationHandlers;

constructor(
openApiSpec: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1,
options: Options = {},
eovOptions: ValidateResponseOpts = {},
serial: number = -1,
afterResponseBodyValidationHandlers?: AfterResponseBodyValidationHandlers,
) {
this.spec = openApiSpec;
this.ajvBody = createResponseAjv(openApiSpec, options);
this.eovOptions = eovOptions;
this.serial = serial;
this.afterResponseBodyValidationHandlers = afterResponseBodyValidationHandlers;

// This is a pseudo-middleware function. It doesn't get registered with
// express via `use`
Expand All @@ -53,6 +57,67 @@ export class ResponseValidator {
}

public validate(): RequestHandler {
if (this.afterResponseBodyValidationHandlers) {
// Use async mung to support async after-response-body-validation hooks
const self = this;
return mung.jsonAsync(async (body, req, res) => {
if (req.openapi && self.serial == req.openapi.serial) {
const openapi = <OpenApiRequestMetadata>req.openapi;
// instead of openapi.schema, use openapi._responseSchema to get the response copy
const responses: OpenAPIV3.ResponsesObject = (<any>openapi)
._responseSchema?.responses;

const validators = self._getOrBuildValidator(req, responses);
const path = req.originalUrl;
const statusCode = res.statusCode;
const contentType = res.getHeaders()['content-type'];
const accept = req.headers['accept'];
// if response has a content type use it, else use accept headers
const accepts: [string] = contentType
? [contentType]
: accept
? accept.split(',').map((h) => h.trim())
: [];

try {
self._validate({
validators,
body,
statusCode,
path,
accepts, // return 406 if not acceptable
});
} catch (err) {
// If a custom error handler was provided, we call that
if (err instanceof InternalServerError && self.eovOptions.onError) {
self.eovOptions.onError(err, body, req);
return body;
} else {
// No custom error handler, or something unexpected happened.
throw err;
}
}

// After successful validation, invoke the per-route after-response hook if configured
const handlerName: string =
openapi.schema['x-eov-after-response-body-validation'] as string;
if (handlerName) {
const handler = self.afterResponseBodyValidationHandlers[handlerName];
if (!handler) {
throw new InternalServerError({
path: path,
message: `afterResponseBodyValidation handler '${handlerName}' not found`,
});
}
const result = await handler(body, req, openapi.schema);
return result !== undefined ? result : body;
}
}
return body;
});
}

// Default synchronous path (no after-response-body-validation handlers configured)
return mung.json((body, req, res) => {
if (req.openapi && this.serial == req.openapi.serial) {
const openapi = <OpenApiRequestMetadata>req.openapi;
Expand All @@ -65,7 +130,7 @@ export class ResponseValidator {
const statusCode = res.statusCode;
const contentType = res.getHeaders()['content-type'];
const accept = req.headers['accept'];
// ir response has a content type use it, else use accept headers
// if response has a content type use it, else use accept headers
const accepts: [string] = contentType
? [contentType]
: accept
Expand Down
58 changes: 55 additions & 3 deletions src/openapi.validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import {
OpenApiRequestMetadata,
ValidateSecurityOpts,
OpenAPIV3,
BeforeRequestBodyValidationHandlers,
AfterResponseBodyValidationHandlers,
InternalServerError,
} from './framework/types';
import { defaultResolver } from './resolvers';
import { OperationHandlerOptions } from './framework/types';
Expand Down Expand Up @@ -189,6 +192,14 @@ export class OpenApiValidator {
});
}

// before request body validation hook middleware
if (this.options.beforeRequestBodyValidation) {
const beforeHookMw = this.beforeRequestBodyValidationMiddleware(
this.options.beforeRequestBodyValidation,
);
middlewares.push(beforeHookMw);
}

// request middleware
if (this.options.validateRequests) {
let reqmw;
Expand All @@ -208,7 +219,11 @@ export class OpenApiValidator {
middlewares.push(function responseMiddleware(req, res, next) {
return pContext
.then(({ responseApiDoc, context: { serial } }) => {
resmw = resmw || self.responseValidationMiddleware(responseApiDoc, serial);
resmw = resmw || self.responseValidationMiddleware(
responseApiDoc,
serial,
self.options.afterResponseBodyValidation,
);
return resmw(req, res, next);
})
.catch(next);
Expand Down Expand Up @@ -291,13 +306,50 @@ export class OpenApiValidator {
return (req, res, next) => requestValidator.validate(req, res, next);
}

private responseValidationMiddleware(apiDoc: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1, serial: number) {
private beforeRequestBodyValidationMiddleware(
handlers: BeforeRequestBodyValidationHandlers,
): OpenApiRequestHandler {
return async function beforeRequestBodyValidationHook(req, res, next) {
try {
if (!req.openapi) {
// Route not matched by OpenAPI spec — skip
return next();
}
const handlerName: string =
req.openapi.schema['x-eov-before-request-body-validation'] as string;
if (!handlerName) {
// No hook configured for this operation
return next();
}
const handler = handlers[handlerName];
if (!handler) {
return next(
new InternalServerError({
path: (req as any).path,
message: `beforeRequestBodyValidation handler '${handlerName}' not found`,
}),
);
}
await handler(req, req.openapi.schema);
next();
} catch (err) {
next(err);
}
};
}

private responseValidationMiddleware(
apiDoc: OpenAPIV3.DocumentV3 | OpenAPIV3.DocumentV3_1,
serial: number,
afterResponseBodyValidation?: AfterResponseBodyValidationHandlers,
) {
return new middlewares.ResponseValidator(
apiDoc,
this.ajvOpts.response,
// This has already been converted from boolean if required
this.options.validateResponses as ValidateResponseOpts,
serial
serial,
afterResponseBodyValidation,
).validate();
}

Expand Down
Loading