Skip to content

Commit

Permalink
intermediate commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Carmine DiMascio committed Mar 24, 2019
1 parent 24878ca commit 4b6c415
Show file tree
Hide file tree
Showing 12 changed files with 356 additions and 28 deletions.
6 changes: 6 additions & 0 deletions TODO.md
@@ -0,0 +1,6 @@
# TODOs

- throw error if path param id's don't match
Note: app.params will not have registered for an unknown path param i.e. when express defines a different path name param than express (we should attempt to detect this and flag it)
- add tests with an indepently defined router
- throw error (e.g 404) when route is defined in express but not in openapi spec
32 changes: 32 additions & 0 deletions openapi.yaml
Expand Up @@ -165,6 +165,38 @@ paths:
schema:
$ref: "#/components/schemas/Error"

/pets/{id}/attributes/{attribute_id}:
get:
description: Returns the attribute specified by attribute_id
operationId: find attributes by pet id
parameters:
- name: id
in: path
description: ID of pet to fetch
required: true
schema:
type: integer
format: int64
- in: path
name: attribute_id
schema:
type: integer
format: int64
required: true
responses:
"200":
description: pet response
content:
application/json:
schema:
$ref: "#/components/schemas/Attribute"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"

/route_not_defined_within_express:
get:
description: Returns attributes for this pet
Expand Down
15 changes: 10 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "express-middleware-openapi",
"version": "0.1.27-alpha",
"version": "0.1.28-alpha",
"description": "",
"main": "dist/index.js",
"scripts": {
Expand All @@ -19,6 +19,7 @@
"openapi-schema-validator": "^3.0.3",
"openapi-security-handler": "^2.0.4",
"openapi-types": "1.3.4",
"path-to-regexp": "^3.0.0",
"ts-log": "^2.1.4"
},
"devDependencies": {
Expand Down
2 changes: 0 additions & 2 deletions src/framework/base.path.ts
Expand Up @@ -9,13 +9,11 @@ export default class BasePath {
// break the url into parts
// baseUrl param added to make the parsing of relative paths go well
const serverUrl = new URL(server.url, 'http://localhost');
console.log(serverUrl);
let urlPath = decodeURI(serverUrl.pathname).replace(/\/$/, '');
if (/{\w+}/.test(urlPath)) {
// has variable that we need to check out
urlPath = urlPath.replace(/{(\w+)}/g, (substring, p1) => `:${p1}`);
}
console.log(urlPath);
this.path = urlPath;
for (const variable in server.variables) {
if (server.variables.hasOwnProperty(variable)) {
Expand Down
65 changes: 57 additions & 8 deletions src/index.ts
Expand Up @@ -9,6 +9,7 @@ import OpenAPIRequestCoercer from 'openapi-request-coercer';
import { OpenAPIFrameworkAPIContext } from './framework/types';
import { methodNotAllowed, notFoundError } from './errors';

import * as middlewares from './middlewares';
// import { OpenAPIResponseValidatorError } from 'openapi-response-validator';
// import { SecurityHandlers } from 'openapi-security-handler';
// import { OpenAPI, OpenAPIV3 } from 'openapi-types';
Expand Down Expand Up @@ -43,6 +44,21 @@ export function OpenApiMiddleware(opts: OpenApiMiddlewareOpts) {
}
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)
);
}

OpenApiMiddleware.prototype.install = function(app: ExpressApp) {
Expand All @@ -57,13 +73,23 @@ OpenApiMiddleware.prototype.install = function(app: ExpressApp) {
}

// 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());
// 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
app.use(
middlewares.core(this.opts, this.apiDoc, this.openApiRouteMap),
middlewares.validateRequest({
loggingKey: this.opts.name,
enableObjectCoercion: this.opts.enableObjectCoercion,
errorTransformer: this.opts.errorTransformer,
})
);

// app.use(unlessDocumented())
};

OpenApiMiddleware.prototype._middleware = function() {
Expand Down Expand Up @@ -143,6 +169,29 @@ OpenApiMiddleware.prototype._transformValidationResult = function(
}
};

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)
Expand All @@ -160,7 +209,7 @@ function createFramework(args: OpenApiMiddlewareOpts): OpenAPIFramework {
...(args as OpenAPIFrameworkArgs),
};

console.log(frameworkArgs);
// console.log(frameworkArgs);
const framework = new OpenAPIFramework(frameworkArgs);
return framework;
}
Expand All @@ -179,8 +228,8 @@ function buildRoutes(framework) {
pathParams.add(param.name);
}
}
const openApiRoute = `${bp.path}${path}`;
const expressRoute = openApiRoute
const openApiRoute = `${path}`;
const expressRoute = `${bp.path}${openApiRoute}`
.split('/')
.map(toExpressParams)
.join('/');
Expand Down
178 changes: 178 additions & 0 deletions src/middlewares/index.ts
@@ -0,0 +1,178 @@
const pathToRegexp = require('path-to-regexp');
const _ = require('lodash');
import OpenAPIRequestValidator from 'openapi-request-validator';
import OpenAPIRequestCoercer from 'openapi-request-coercer';
import { methodNotAllowed, notFoundError } from '../errors';

export function core(opts, apiDoc, openApiRouteMap) {
return (req, res, next) => {
req.openapi = {
apiDoc,
};
const matched = matchRoute(req);

if (matched) {
const { expressRoute, openApiRoute, pathParams } = matched;
console.log('core_mw: matched', expressRoute, openApiRoute);
req.openapi.expressRoute = expressRoute;
req.openapi.openApiRoute = openApiRoute;
req.params = pathParams;
req.openapi.pathParams = pathParams;
next();
} else {
res.status(404).json({ test: 'test' });
}
};

function matchRoute(req) {
const path = req.path;
const method = req.method;
for (const [openApiRoute, methods] of Object.entries(openApiRouteMap)) {
const { expressRoute } = methods[method];
console.log('core_mw: matchRoute', openApiRoute, expressRoute, req.path); //, methods[method]);
const keys = [];
const regexp = pathToRegexp(methods[method].expressRoute, keys);
const matchedRoute = regexp.exec(path);

if (matchedRoute) {
console.log('core_mw: matchRoute', matchedRoute);
console.log('core_mw: matchRoute:keys', keys);
// TODO is this a good enough test
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 {
expressRoute,
openApiRoute,
pathParams,
};
}
}

return null;
}
}

export function validateRequest({
loggingKey,
enableObjectCoercion,
errorTransformer,
}) {
return (req, res, next) => {
const { path: rpath, method, route } = req;
console.log(
'validateRequest_mw: ',
rpath,
route,
method,
req.openapi.openApiRoute
);
const path = req.openapi.openApiRoute;
if (path && method) {
const documentedRoute = req.openapi.apiDoc.paths[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 } = sendValidationResultError(
res,
notFoundError(path),
errorTransformer
);
return res.status(statusCode).json(error);
}

const schema = documentedRoute[method.toLowerCase()];
if (!schema) {
const { statusCode, error } = sendValidationResultError(
res,
methodNotAllowed(path, method),
errorTransformer
);
return res.status(statusCode).json(error);
}

console.log('validateRequest_mw: schema', schema);
// TODO coercer and request validator fail on null parameters
if (!schema.parameters) {
schema.parameters = [];
}

console.log(
'----about to coerce',
req.params,
'-',
req.openapi.pathParams
);
if (Object.keys(req.params).length === 0 && req.openapi.pathParams) {
console.log('-----------OVERRIDING PATH PARAMS');
req.params = req.openapi.pathParams;
}
// 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);
}

console.log('----about to validate', req.params);
const validationResult = new OpenAPIRequestValidator({
errorTransformer,
parameters: schema.parameters || [],
requestBody: schema.requestBody,
// schemas: this.apiDoc.definitions, // v2
componentSchemas: req.openapi.apiDoc.components // v3
? req.openapi.apiDoc.components.schemas
: undefined,
}).validate(req);

if (validationResult && validationResult.errors.length > 0) {
const { statusCode, error } = sendValidationResultError(
res,
validationResult,
errorTransformer
);
return res.status(statusCode).json(error);
}
}
next();
};
}

// function transformValidationResult(validationResult, transformer) {
function sendValidationResultError(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}'
);
}
return res.status(x.statusCode).json(x.error);
}

// function identifyRoutePath(route, path) {
// return Array.isArray(route.path)
// ? route.path.find(r => r === path)
// : route.path || path;
// }

export function httpNotFound(req, res, next) {
// if (req.openapi.path)
}

0 comments on commit 4b6c415

Please sign in to comment.