Skip to content

Commit

Permalink
Resolvers can now be specified through operationHandlers as a Operati…
Browse files Browse the repository at this point in the history
…onHandlerOptions
  • Loading branch information
mdwheele committed Jun 2, 2020
1 parent 229f4b8 commit 7d50628
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 58 deletions.
30 changes: 16 additions & 14 deletions examples/5-custom-operation-resolver/app.js
Expand Up @@ -25,21 +25,23 @@ app.use('/spec', express.static(apiSpec));
new OpenApiValidator({
apiSpec,
validateResponses: true, // default false
// 3. Provide the path to the controllers directory
operationHandlers: path.join(__dirname, 'routes'), // default false,
// 4. Provide a function responsible for resolving an Express RequestHandler
// function from the current OpenAPI Route object.
operationResolver: function (basePath, route) {
[controller, method] = route.schema['operationId'].split('.')

const modulePath = path.join(basePath, controller);
const handler = require(modulePath)

if (handler[method] === undefined) {
throw new Error(`Couldn't find a [${method}] function in ${modulePath} when trying to route [${route.method} ${route.expressRoute}].`)
operationHandlers: {
// 3. Provide the path to the controllers directory
basePath: path.join(__dirname, 'routes'),
// 4. Provide a function responsible for resolving an Express RequestHandler
// function from the current OpenAPI Route object.
resolver: function (basePath, route) {
[controller, method] = route.schema['operationId'].split('.')

const modulePath = path.join(basePath, controller);
const handler = require(modulePath)

if (handler[method] === undefined) {
throw new Error(`Couldn't find a [${method}] function in ${modulePath} when trying to route [${route.method} ${route.expressRoute}].`)
}

return handler[method]
}

return handler[method]
}
})
.install(app)
Expand Down
8 changes: 6 additions & 2 deletions src/framework/types.ts
Expand Up @@ -47,6 +47,11 @@ export type ValidateSecurityOpts = {
handlers?: SecurityHandlers;
};

export type OperationHandlerOptions = {
basePath: string,
resolver: Function
}

export interface OpenApiValidatorOpts {
apiSpec: OpenAPIV3.Document | string;
validateResponses?: boolean | ValidateResponseOpts;
Expand All @@ -61,8 +66,7 @@ export interface OpenApiValidatorOpts {
$refParser?: {
mode: 'bundle' | 'dereference';
};
operationHandlers?: false | string;
operationResolver?: Function;
operationHandlers?: false | string | OperationHandlerOptions;
validateFormats?: false | 'fast' | 'full';
}

Expand Down
76 changes: 34 additions & 42 deletions src/index.ts
Expand Up @@ -16,6 +16,8 @@ import {
import { deprecationWarning } from './middlewares/util';
import * as path from 'path';
import { BasePath } from './framework/base.path';
import { defaultResolver } from './resolvers';
import { OperationHandlerOptions } from './framework/types'

export {
InternalServerError,
Expand All @@ -42,47 +44,22 @@ export class OpenApiValidator {
if (options.validateSecurity == null) options.validateSecurity = true;
if (options.fileUploader == null) options.fileUploader = {};
if (options.$refParser == null) options.$refParser = { mode: 'bundle' };
if (options.operationHandlers == null) options.operationHandlers = false;
if (options.validateFormats == null) options.validateFormats = 'fast';

if (options.operationResolver == null) {
options.operationResolver = (handlersPath: false | string, route: RouteMetadata) => {
const tmpModules = {};
const { expressRoute, method, schema } = route;
const oId = schema['x-eov-operation-id'] || schema['operationId'];
const baseName = schema['x-eov-operation-handler'];
if (oId && !baseName) {
throw Error(
`found x-eov-operation-id for route ${method} - ${expressRoute}]. x-eov-operation-handler required.`,
);
}
if (!oId && baseName) {
throw Error(
`found x-eov-operation-handler for route [${method} - ${expressRoute}]. operationId or x-eov-operation-id required.`,
);
}
if (
oId &&
baseName &&
typeof handlersPath === 'string'
) {
const modulePath = path.join(handlersPath, baseName);
if (!tmpModules[modulePath]) {
tmpModules[modulePath] = require(modulePath);
if (!tmpModules[modulePath][oId]) {
// if oId is not found only module, try the module's default export
tmpModules[modulePath] = tmpModules[modulePath].default;
}
}
if (!tmpModules[modulePath][oId]) {
throw Error(
`Could not find 'x-eov-operation-handler' with id ${oId} in module '${modulePath}'. Make sure operation '${oId}' defined in your API spec exists as a handler function in '${modulePath}'.`,
);
}
return tmpModules[modulePath][oId];
}

if (typeof options.operationHandlers === 'string') {
/**
* Internally, we want to convert this to a value typed OperationHandlerOptions.
* In this way, we can treat the value as such when we go to install (rather than
* re-interpreting it over and over).
*/
options.operationHandlers = {
basePath: options.operationHandlers,
resolver: defaultResolver
}
}
} else if (typeof options.operationHandlers !== 'object') {
// This covers cases where operationHandlers is null, undefined or false.
options.operationHandlers = false
}

if (options.validateResponses === true) {
options.validateResponses = {
Expand Down Expand Up @@ -283,9 +260,16 @@ export class OpenApiValidator {
for (const route of context.routes) {
const { method, expressRoute } = route;

const fn = this.options.operationResolver(this.options.operationHandlers, route);

app[method.toLowerCase()](expressRoute, fn);
/**
* This if-statement is here to "narrow" the type of options.operationHanlders
* to OperationHandlerOptions (down from string | false | OperationHandlerOptions)
* At this point of execution it _should_ be impossible for this to NOT be the correct
* type as we re-assign during construction to verify this.
*/
if (this.isOperationHandlerOptions(this.options.operationHandlers)) {
const { basePath, resolver } = this.options.operationHandlers
app[method.toLowerCase()](expressRoute, resolver(basePath, route));
}
}
}

Expand Down Expand Up @@ -355,4 +339,12 @@ export class OpenApiValidator {
delete options.multerOpts;
}
}

private isOperationHandlerOptions(value: false | string | OperationHandlerOptions): value is OperationHandlerOptions {
if ((value as OperationHandlerOptions).resolver) {
return true
} else {
return false
}
}
}
40 changes: 40 additions & 0 deletions src/resolvers.ts
@@ -0,0 +1,40 @@
import * as path from 'path';
import { RequestHandler } from "express";
import { RouteMetadata } from "./framework/openapi.spec.loader";

export function defaultResolver(handlersPath: string, route: RouteMetadata): RequestHandler {
const tmpModules = {};
const { expressRoute, method, schema } = route;
const oId = schema['x-eov-operation-id'] || schema['operationId'];
const baseName = schema['x-eov-operation-handler'];
if (oId && !baseName) {
throw Error(
`found x-eov-operation-id for route ${method} - ${expressRoute}]. x-eov-operation-handler required.`,
);
}
if (!oId && baseName) {
throw Error(
`found x-eov-operation-handler for route [${method} - ${expressRoute}]. operationId or x-eov-operation-id required.`,
);
}
if (
oId &&
baseName &&
typeof handlersPath === 'string'
) {
const modulePath = path.join(handlersPath, baseName);
if (!tmpModules[modulePath]) {
tmpModules[modulePath] = require(modulePath);
if (!tmpModules[modulePath][oId]) {
// if oId is not found only module, try the module's default export
tmpModules[modulePath] = tmpModules[modulePath].default;
}
}
if (!tmpModules[modulePath][oId]) {
throw Error(
`Could not find 'x-eov-operation-handler' with id ${oId} in module '${modulePath}'. Make sure operation '${oId}' defined in your API spec exists as a handler function in '${modulePath}'.`,
);
}
return tmpModules[modulePath][oId];
}
}

0 comments on commit 7d50628

Please sign in to comment.