-
-
Notifications
You must be signed in to change notification settings - Fork 208
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Carmine DiMascio
committed
Mar 24, 2019
1 parent
4b6c415
commit f0754f6
Showing
7 changed files
with
48 additions
and
414 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,249 +1,60 @@ | ||
import * as _ from 'lodash'; | ||
import { ExpressApp } from 'express'; | ||
import OpenAPIFramework, { | ||
OpenAPIFrameworkArgs, | ||
OpenAPIFrameworkConstructorArgs, | ||
} from './framework'; | ||
import OpenAPIRequestValidator from 'openapi-request-validator'; | ||
import OpenAPIRequestCoercer from 'openapi-request-coercer'; | ||
import { OpenAPIFrameworkAPIContext } from './framework/types'; | ||
import { methodNotAllowed, notFoundError } from './errors'; | ||
|
||
import { OpenAPIFrameworkArgs } from './framework'; | ||
import { OpenApiContext } from './openapi.context'; | ||
import * as middlewares from './middlewares'; | ||
// import { OpenAPIResponseValidatorError } from 'openapi-response-validator'; | ||
// import { SecurityHandlers } from 'openapi-security-handler'; | ||
// import { OpenAPI, OpenAPIV3 } from 'openapi-types'; | ||
|
||
export interface ErrorResponse { | ||
statusCode: number; | ||
error: any; | ||
} | ||
|
||
export interface OpenApiMiddlewareOpts extends OpenAPIFrameworkArgs { | ||
name: string; | ||
apiSpecPath: string; | ||
errorTransform?: (validationResult: any) => ErrorResponse; | ||
errorTransformer?: (validationResult: any) => ErrorResponse; | ||
} | ||
|
||
export function OpenApiMiddleware(opts: OpenApiMiddlewareOpts) { | ||
if (!opts.apiSpecPath) throw new Error('apiSpecPath required'); | ||
|
||
opts.enableObjectCoercion = opts.enableObjectCoercion || true; | ||
opts.name = opts.name || 'express-middleware-openapi'; | ||
|
||
const framework = createFramework({ ...opts, apiDoc: opts.apiSpecPath }); | ||
const contextOpts = { ...opts, apiDoc: opts.apiSpecPath }; | ||
const openApiContext = new OpenApiContext(contextOpts); | ||
|
||
this.opts = opts; | ||
this.apiDoc = framework.apiDoc; | ||
this.routes = buildRoutes(framework); | ||
this.routeMap = this.routes.reduce((a, r) => { | ||
const routeMethod = a[r.expressRoute]; | ||
if (routeMethod) { | ||
routeMethod[r.method] = r.schema; | ||
} else { | ||
a[r.expressRoute] = { [r.method]: r.schema }; | ||
} | ||
return a; | ||
}, {}); | ||
|
||
this.openApiRouteMap = this.routes.reduce((a, r) => { | ||
const routeMethod = a[r.openApiRoute]; | ||
const schema = { ...r.schema, expressRoute: r.expressRoute }; | ||
if (routeMethod) { | ||
routeMethod[r.method] = schema; | ||
} else { | ||
a[r.openApiRoute] = { [r.method]: schema }; | ||
} | ||
return a; | ||
}, {}); | ||
|
||
this.routeMatchRegex = buildRouteMatchRegex( | ||
this.routes.map(r => r.expressRoute) | ||
); | ||
this.apiDoc = openApiContext.apiDoc; | ||
this.expressRouteMap = openApiContext.expressRouteMap; | ||
this.context = openApiContext; | ||
} | ||
|
||
OpenApiMiddleware.prototype.install = function(app: ExpressApp) { | ||
const noPathParamRoutes = []; | ||
const pathParms = []; | ||
for (const route of this.routes) { | ||
if (route.pathParams.length === 0) { | ||
noPathParamRoutes.push(route.expressRoute); | ||
} else { | ||
pathParms.push(...route.pathParams); | ||
const pathParams = []; | ||
for (const route of this.context.routes) { | ||
if (route.pathParams.length > 0) { | ||
pathParams.push(...route.pathParams); | ||
} | ||
} | ||
|
||
// install param on routes with paths | ||
// for (const p of _.uniq(pathParms)) { | ||
// app.param(p, this._middleware()); | ||
// } | ||
// // install use on routes without paths | ||
// app.all(_.uniq(noPathParamRoutes), this._middleware()); | ||
|
||
// TODOD add middleware to capture routes not defined in openapi spec and throw not 404 | ||
for (const p of _.uniq(pathParams)) { | ||
app.param(p, (req, res, next, value, name) => { | ||
console.log(name, value); | ||
if (req.openapi.pathParams) { | ||
// override path params | ||
req.params[name] = req.openapi.pathParams[name] || req.params[name]; | ||
} | ||
next(); | ||
}); | ||
} | ||
app.use( | ||
middlewares.core(this.opts, this.apiDoc, this.openApiRouteMap), | ||
middlewares.applyOpenApiMetadata(this.context), | ||
middlewares.validateRequest({ | ||
apiDoc: this.apiDoc, | ||
loggingKey: this.opts.name, | ||
enableObjectCoercion: this.opts.enableObjectCoercion, | ||
errorTransformer: this.opts.errorTransformer, | ||
}) | ||
); | ||
|
||
// app.use(unlessDocumented()) | ||
}; | ||
|
||
OpenApiMiddleware.prototype._middleware = function() { | ||
return (req, res, next) => { | ||
const { path: rpath, method, route } = req; | ||
const path = identifyRoutePath(route, rpath); | ||
if (path && method) { | ||
const documentedRoute = this.routeMap[path]; | ||
if (!documentedRoute) { | ||
// TODO add option to enable undocumented routes to pass through without 404 | ||
// TODO this should not occur as we only set up middleware and params on routes defined in the openapi spec | ||
const { statusCode, error } = this._transformValidationResult( | ||
notFoundError(path) | ||
); | ||
return res.status(statusCode).json(error); | ||
} | ||
|
||
const schema = documentedRoute[method.toUpperCase()]; | ||
if (!schema) { | ||
const { statusCode, error } = this._transformValidationResult( | ||
methodNotAllowed(path, method) | ||
); | ||
return res.status(statusCode).json(error); | ||
} | ||
|
||
// this req matched an openapi route, mark it | ||
req.openapi = {}; | ||
|
||
// TODO coercer and request validator fail on null parameters | ||
if (!schema.parameters) { | ||
schema.parameters = []; | ||
} | ||
|
||
// Check if route is in map (throw error - option to ignore) | ||
if (this.opts.enableObjectCoercion) { | ||
// this modifies the request object with coerced types | ||
new OpenAPIRequestCoercer({ | ||
loggingKey: this.opts.name, | ||
enableObjectCoercion: this.opts.enableObjectCoercion, | ||
parameters: schema.parameters, | ||
}).coerce(req); | ||
} | ||
|
||
const validationResult = new OpenAPIRequestValidator({ | ||
errorTransformer: this.errorTransformer, | ||
parameters: schema.parameters || [], | ||
requestBody: schema.requestBody, | ||
// schemas: this.apiDoc.definitions, // v2 | ||
componentSchemas: this.apiDoc.components // v3 | ||
? this.apiDoc.components.schemas | ||
: undefined, | ||
}).validate(req); | ||
|
||
if (validationResult && validationResult.errors.length > 0) { | ||
const { statusCode, error } = this._transformValidationResult( | ||
validationResult | ||
); | ||
return res.status(statusCode).json(error); | ||
} | ||
} | ||
next(); | ||
}; | ||
}; | ||
|
||
OpenApiMiddleware.prototype._transformValidationResult = function( | ||
validationResult | ||
) { | ||
if (validationResult && validationResult.errors.length > 0) { | ||
const transform = | ||
this.opts.errorTransform || | ||
(v => ({ | ||
statusCode: v.status, | ||
error: { errors: v.errors }, | ||
})); | ||
|
||
return transform(validationResult); | ||
} | ||
}; | ||
|
||
function unlessDocumented(middleware, documentedRoutes) { | ||
const re = buildRouteMatchRegex(documentedRoutes); | ||
return (req, res, next) => { | ||
const isDocumented = path => re.test(path); | ||
if (isDocumented(req.path)) { | ||
return next(); | ||
} else { | ||
return middleware(req, res, next); | ||
} | ||
}; | ||
} | ||
function buildRouteMatchRegex(routes) { | ||
const matchers = routes | ||
.map(route => { | ||
return `^${route}/?$`; | ||
}) | ||
.join('|'); | ||
|
||
console.log(matchers); | ||
return new RegExp(`^(?!${matchers})`); | ||
|
||
// ^(?!^\/test/?$|^\/best/:id/?$|^\/yo/?$)(.*)$ | ||
} | ||
function identifyRoutePath(route, path) { | ||
return Array.isArray(route.path) | ||
? route.path.find(r => r === path) | ||
: route.path || path; | ||
} | ||
|
||
function toExpressParams(part) { | ||
return part.replace(/\{([^}]+)}/g, ':$1'); | ||
} | ||
|
||
function createFramework(args: OpenApiMiddlewareOpts): OpenAPIFramework { | ||
const frameworkArgs: OpenAPIFrameworkConstructorArgs = { | ||
featureType: 'middleware', | ||
name: args.name, | ||
...(args as OpenAPIFrameworkArgs), | ||
}; | ||
|
||
// console.log(frameworkArgs); | ||
const framework = new OpenAPIFramework(frameworkArgs); | ||
return framework; | ||
} | ||
|
||
function buildRoutes(framework) { | ||
const routes = []; | ||
framework.initialize({ | ||
visitApi(ctx: OpenAPIFrameworkAPIContext) { | ||
const apiDoc = ctx.getApiDoc(); | ||
for (const bp of ctx.basePaths) { | ||
for (const [path, methods] of Object.entries(apiDoc.paths)) { | ||
for (const [method, schema] of Object.entries(methods)) { | ||
const pathParams = new Set(); | ||
for (const param of schema.parameters || []) { | ||
if (param.in === 'path') { | ||
pathParams.add(param.name); | ||
} | ||
} | ||
const openApiRoute = `${path}`; | ||
const expressRoute = `${bp.path}${openApiRoute}` | ||
.split('/') | ||
.map(toExpressParams) | ||
.join('/'); | ||
routes.push({ | ||
expressRoute, | ||
openApiRoute, | ||
method: method.toUpperCase(), | ||
pathParams: Array.from(pathParams), | ||
schema, | ||
}); | ||
} | ||
} | ||
} | ||
}, | ||
}); | ||
return routes; | ||
} |
Oops, something went wrong.