Skip to content

Commit

Permalink
feat(rest): use phase to manage express middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Sep 7, 2018
1 parent 00d0138 commit 6c1be3a
Show file tree
Hide file tree
Showing 3 changed files with 259 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/rest/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@loopback/http-server": "^0.3.5",
"@loopback/openapi-v3": "^0.12.6",
"@loopback/openapi-v3-types": "^0.8.5",
"@loopback/phase": "^0.1.0",
"@types/cors": "^2.8.3",
"@types/express": "^4.11.1",
"@types/http-errors": "^1.6.1",
Expand Down
132 changes: 132 additions & 0 deletions packages/rest/src/middleware-phase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright IBM Corp. 2017,2018. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Context} from '@loopback/context';
import {PhaseList, HandlerChain} from '@loopback/phase';
import {RequestHandler, ErrorRequestHandler} from 'express';
import {RequestContext} from './request-context';
import {PathParams} from 'express-serve-static-core';
import * as pathToRegExp from 'path-to-regexp';
const debug = require('debug')('loopback:rest:middleware');

/**
* A registry for Express middleware by phase
*/
export class MiddlewareList {
private phaseList: PhaseList;
constructor(phaseNames: string[] = []) {
this.phaseList = new PhaseList(phaseNames);
}

/**
* Register handlers by phase
* @param phaseName Name of the phase
* @param path Path filter
* @param handlers Middleware handlers
*/
middleware(
phaseName: string,
path: PathParams,
...handlers: RequestHandler[]
): this {
const re = pathToRegExp(path, [], {end: false});
for (const handler of handlers) {
this.phaseList.registerHandler(
phaseName,
async (ctx: RequestContext, chain: HandlerChain) => {
debug('Request: %s pattern: %s', ctx.request.url, re);
if (!re.test(ctx.request.path)) {
debug('Skipping %s', ctx.request.path);
await chain.next();
return;
}
debug('Invoking middleware %s', handler.name);
let nextPromise: Promise<void> | undefined = undefined;
// tslint:disable-next-line:no-any
handler(ctx.request, ctx.response, (err?: any) => {
// Keep the callback as a sync function as expected by express
// middleware
if (!err) {
// Track the result of chain.next as it can be rejected
nextPromise = chain.next();
} else {
chain.throw(err);
}
});
// Catch the rejected promise if necessary
// tslint:disable-next-line:await-promise
if (nextPromise) await nextPromise;
if (!chain.done) chain.stop();
},
);
}
return this;
}

/**
*
* @param path
* @param handlers
*/
finalMiddleware(path: PathParams, ...handlers: RequestHandler[]) {
this.middleware(this.phaseList.finalPhase.id, path, ...handlers);
}

/**
*
* @param path
* @param handlers
*/
errorMiddleware(path: PathParams, ...handlers: ErrorRequestHandler[]) {
const re = pathToRegExp(path, [], {end: false});
for (const handler of handlers) {
this.phaseList.registerHandler(
this.phaseList.errorPhase.id,
// tslint:disable-next-line:no-any
async (ctx: RequestContext & {error?: any}, chain: HandlerChain) => {
debug('Request: %s pattern: %s', ctx.request.url, re);
if (!re.test(ctx.request.path)) {
debug('Skipping %s', ctx.request.path);
await chain.next();
return;
}
debug('Invoking error middleware %s', handler.name);
let nextPromise: Promise<void> | undefined = undefined;
// tslint:disable-next-line:no-any
handler(ctx.error, ctx.request, ctx.response, (err?: any) => {
// Keep the callback as a sync function as expected by express
// middleware
if (!err) {
// Track the result of chain.next as it can be rejected
nextPromise = chain.next();
} else {
chain.throw(err);
}
});
// Catch the rejected promise if necessary
// tslint:disable-next-line:await-promise
if (nextPromise) await nextPromise;
if (!chain.done) chain.stop();
},
);
}
return this;
}

/**
* Create an express middleware from the registry
*/
asHandler(): RequestHandler {
return async (req, res, next) => {
const reqCtx = new RequestContext(req, res, new Context());
try {
await this.phaseList.run(reqCtx);
next();
} catch (e) {
next(e);
}
};
}
}
126 changes: 126 additions & 0 deletions packages/rest/test/integration/middleware-phase.integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/rest
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import * as express from 'express';
import {MiddlewareList} from '../../src/middleware-phase';
import {RequestHandler, ErrorRequestHandler} from 'express-serve-static-core';
import {supertest, expect} from '@loopback/testlab';

describe('Express middleware phases', () => {
let app: express.Application;
let middlewareChain: MiddlewareList;

beforeEach(() => {
app = express();
middlewareChain = new MiddlewareList(['initial', 'auth', 'route']);
});

it('registers middleware by phase', async () => {
const steps: string[] = [];
middlewareChain.middleware(
'route',
'/',
givenMiddleware('route-1', steps),
givenMiddleware('route-2', steps),
);
middlewareChain.middleware(
'initial',
'/',
givenMiddleware('initial-1', steps),
givenMiddleware('initial-2', steps),
);
app.use(middlewareChain.asHandler());
app.use((req, res) => {
res.json({steps});
});

await supertest(app)
.get('/')
.expect(200, {steps});

expect(steps).to.eql(['initial-1', 'initial-2', 'route-1', 'route-2']);
});

it('registers middleware by phase and path', async () => {
const steps: string[] = [];
middlewareChain.middleware(
'route',
'/foo',
givenMiddleware('route-1', steps),
);
middlewareChain.middleware(
'initial',
'/',
givenMiddleware('initial-1', steps),
givenMiddleware('initial-2', steps),
);
middlewareChain.middleware(
'route',
'/bar',
givenMiddleware('route-2', steps),
);
app.use(middlewareChain.asHandler());
app.use((req, res) => {
res.json({steps});
});

const test = supertest(app);
await test.get('/foo').expect(200, {steps});
expect(steps).to.eql(['initial-1', 'initial-2', 'route-1']);

// Reset steps
steps.splice(0, steps.length);
await test.get('/bar').expect(200, {steps});
expect(steps).to.eql(['initial-1', 'initial-2', 'route-2']);
});

it('registers error and final middleware', async () => {
const steps: string[] = [];
middlewareChain.middleware(
'route',
'/foo',
givenMiddleware('route-1', steps),
);
middlewareChain.middleware(
'initial',
'/',
givenMiddleware('initial-1', steps),
);
middlewareChain.middleware('auth', '/foo', (req, res, next) => {
steps.push('auth');
next(new Error('not authenticated'));
});

middlewareChain.errorMiddleware(
'/',
givenErrorMiddleware('error-1', steps),
);
middlewareChain.finalMiddleware('/', givenMiddleware('final-1', steps));

app.use(middlewareChain.asHandler());

const test = supertest(app);
await test.get('/foo').expect(401);

expect(steps).to.eql(['initial-1', 'auth', 'error-1', 'final-1']);
});
});

function givenMiddleware(name: string, steps: string[]): RequestHandler {
return (req, res, next) => {
steps.push(name);
next();
};
}

function givenErrorMiddleware(
name: string,
steps: string[],
): ErrorRequestHandler {
return (err, req, res, next) => {
steps.push(name);
res.status(401).json({error: err.message});
};
}

0 comments on commit 6c1be3a

Please sign in to comment.