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
f0754f6
commit 549192e
Showing
4 changed files
with
286 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
const pathToRegexp = require('path-to-regexp'); | ||
const _ = require('lodash'); | ||
import { OpenApiContext } from '../openapi.context'; | ||
|
||
export function applyOpenApiMetadata(openApiContext: OpenApiContext) { | ||
return (req, res, next) => { | ||
req.openapi = {}; | ||
console.log('applyOpenApiMetadata: applying metadata to request', req.path); | ||
const matched = matchRoute(req); | ||
|
||
if (matched) { | ||
const { expressRoute, openApiRoute, pathParams, schema } = matched; | ||
console.log('applyOpenApiMetadata', expressRoute, openApiRoute); | ||
req.openapi.expressRoute = expressRoute; | ||
req.openapi.openApiRoute = openApiRoute; | ||
req.openapi.pathParams = pathParams; | ||
req.openapi.schema = schema; | ||
req.params = pathParams; | ||
} | ||
next(); | ||
}; | ||
|
||
function matchRoute(req) { | ||
const path = req.path; | ||
const method = req.method; | ||
for (const [expressRoute, methods] of Object.entries( | ||
openApiContext.expressRouteMap | ||
)) { | ||
const schema = methods[method]; | ||
const routePair = openApiContext.routePair(expressRoute); | ||
const openApiRoute = routePair.openApiRoute; | ||
|
||
const keys = []; | ||
const regexp = pathToRegexp(expressRoute, keys); | ||
const matchedRoute = regexp.exec(path); | ||
|
||
if (matchedRoute) { | ||
console.log('core_mw: matchRoute', matchedRoute); | ||
console.log('core_mw: matchRoute:keys', keys); | ||
const paramKeys = keys.map(k => k.name); | ||
const paramsVals = matchedRoute.slice(1); | ||
const pathParams = _.zipObject(paramKeys, paramsVals); | ||
console.log('core_mw: create params', pathParams); | ||
return { | ||
schema, | ||
// schema may or may not contain express and openApi routes, so include them here | ||
expressRoute, | ||
openApiRoute, | ||
pathParams, | ||
}; | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
} |
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 |
---|---|---|
@@ -0,0 +1,91 @@ | ||
import OpenAPIRequestValidator from 'openapi-request-validator'; | ||
import OpenAPIRequestCoercer from 'openapi-request-coercer'; | ||
import { methodNotAllowed, notFoundError } from '../errors'; | ||
|
||
export function validateRequest({ | ||
apiDoc, | ||
loggingKey, | ||
enableObjectCoercion, | ||
errorTransformer, | ||
}) { | ||
return (req, res, next) => { | ||
console.log( | ||
'validateRequest_mw: ', | ||
req.openapi.openApiRoute, | ||
req.openapi.expressRoute | ||
); | ||
const path = req.openapi.expressRoute; | ||
if (!path) { | ||
const err = notFoundError(req.path); | ||
return sendValidationError(res, err, errorTransformer); | ||
} | ||
const schema = req.openapi.schema; | ||
if (!schema) { | ||
// add openapi metadata to make this case more clear | ||
// its not obvious that missig schema means methodNotAllowed | ||
const err = methodNotAllowed(path, req.method); | ||
return sendValidationError(res, err, errorTransformer); | ||
} | ||
|
||
if (!schema.parameters) { | ||
schema.parameters = []; | ||
} | ||
|
||
const shouldUpdatePathParams = | ||
Object.keys(req.openapi.pathParams).length > 0; | ||
|
||
if (shouldUpdatePathParams) { | ||
req.params = req.openapi.pathParams || req.params; | ||
} | ||
|
||
// Check if route is in map (throw error - option to ignore) | ||
if (enableObjectCoercion) { | ||
// this modifies the request object with coerced types | ||
new OpenAPIRequestCoercer({ | ||
loggingKey, | ||
enableObjectCoercion, | ||
parameters: schema.parameters, | ||
}).coerce(req); | ||
} | ||
|
||
const validationResult = new OpenAPIRequestValidator({ | ||
// errorTransformer, // TODO create custom error transformere here as there are a lot of props we can utilize | ||
parameters: schema.parameters || [], | ||
requestBody: schema.requestBody, | ||
// schemas: this.apiDoc.definitions, // v2 | ||
componentSchemas: apiDoc.components // v3 | ||
? apiDoc.components.schemas | ||
: undefined, | ||
}).validate(req); | ||
|
||
if (validationResult && validationResult.errors.length > 0) { | ||
return sendValidationError(res, validationResult, errorTransformer); | ||
} | ||
next(); | ||
}; | ||
} | ||
|
||
function sendValidationError(res, validationResult, transformer) { | ||
console.log( | ||
'validateRequest_mw: validation error', | ||
validationResult, | ||
transformer | ||
); | ||
if (!validationResult) throw Error('validationResult missing'); | ||
|
||
const transform = | ||
transformer || | ||
(v => ({ | ||
statusCode: v.status, | ||
// TODO content-type shoudl be set and retuned | ||
error: { errors: v.errors }, | ||
})); | ||
const x = transform(validationResult); | ||
if (!x || !x.statusCode || !x.error) { | ||
throw Error( | ||
'invalid error transform. must return an object with shape { statusCode, error}' | ||
); | ||
} | ||
// TODO throw rather than returning a result | ||
return res.status(x.statusCode).json(x.error); | ||
} |
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 |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { OpenApiSpecLoader } from './openapi.spec.loader'; | ||
import { OpenAPIFrameworkArgs } from './framework'; | ||
|
||
export class OpenApiContext { | ||
// TODO cleanup structure (group related functionality) | ||
expressRouteMap = {}; | ||
openApiRouteMap = {}; | ||
routes = []; | ||
apiDoc; | ||
constructor(opts: OpenAPIFrameworkArgs) { | ||
const openApiRouteDiscovery = new OpenApiSpecLoader(opts); | ||
const { apiDoc, routes } = openApiRouteDiscovery.load(); | ||
|
||
this.apiDoc = apiDoc; | ||
this.routes = this.initializeRoutes(routes); | ||
} | ||
|
||
private initializeRoutes(routes) { | ||
for (const route of routes) { | ||
const routeMethods = this.expressRouteMap[route.expressRoute]; | ||
if (routeMethods) { | ||
routeMethods[route.method] = route.schema; | ||
} else { | ||
const { schema, openApiRoute, expressRoute } = route; | ||
const routeMethod = { [route.method]: schema }; | ||
const routeDetails = { | ||
_openApiRoute: openApiRoute, | ||
_expressRoute: expressRoute, | ||
...routeMethod, | ||
}; | ||
this.expressRouteMap[route.expressRoute] = routeDetails; | ||
this.openApiRouteMap[route.openApiRoute] = routeDetails; | ||
} | ||
} | ||
return routes; | ||
} | ||
|
||
routePair(route) { | ||
const methods = this.methods(route); | ||
if (methods) { | ||
return { | ||
expressRoute: methods._expressRoute, | ||
openApiRoute: methods._openApiRoute, | ||
}; | ||
} | ||
return null; | ||
} | ||
|
||
methods(route) { | ||
const expressRouteMethods = this.expressRouteMap[route]; | ||
if (expressRouteMethods) return expressRouteMethods; | ||
const openApiRouteMethods = this.openApiRouteMap[route]; | ||
return openApiRouteMethods; | ||
} | ||
|
||
schema(route, method) { | ||
const methods = this.methods(route); | ||
if (methods) { | ||
const schema = methods[method]; | ||
return schema; | ||
} | ||
return null; | ||
} | ||
} |
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 |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import * as _ from 'lodash'; | ||
|
||
import OpenAPIFramework, { | ||
OpenAPIFrameworkArgs, | ||
OpenAPIFrameworkConstructorArgs, | ||
} from './framework'; | ||
import { OpenAPIFrameworkAPIContext } from './framework/types'; | ||
|
||
export class OpenApiSpecLoader { | ||
private opts: OpenAPIFrameworkArgs; | ||
constructor(opts: OpenAPIFrameworkArgs) { | ||
this.opts = opts; | ||
} | ||
|
||
load() { | ||
const framework = this.createFramework(this.opts); | ||
const apiDoc = framework.apiDoc; | ||
const routes = this.discoverRoutes(framework); | ||
return { | ||
apiDoc, | ||
routes, | ||
}; | ||
} | ||
|
||
private createFramework(args: OpenAPIFrameworkArgs): OpenAPIFramework { | ||
const frameworkArgs: OpenAPIFrameworkConstructorArgs = { | ||
featureType: 'middleware', | ||
name: 'express-middleware-openapi', | ||
...(args as OpenAPIFrameworkArgs), | ||
}; | ||
|
||
console.log(frameworkArgs); | ||
const framework = new OpenAPIFramework(frameworkArgs); | ||
return framework; | ||
} | ||
|
||
private discoverRoutes(framework) { | ||
const routes = []; | ||
const toExpressParams = this.toExpressParams; | ||
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 = `${bp.path}${path}`; | ||
const expressRoute = `${openApiRoute}` | ||
.split('/') | ||
.map(toExpressParams) | ||
.join('/'); | ||
routes.push({ | ||
expressRoute, | ||
openApiRoute, | ||
method: method.toUpperCase(), | ||
pathParams: Array.from(pathParams), | ||
schema, | ||
}); | ||
} | ||
} | ||
} | ||
}, | ||
}); | ||
return routes; | ||
} | ||
|
||
private toExpressParams(part) { | ||
return part.replace(/\{([^}]+)}/g, ':$1'); | ||
} | ||
} |