-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rest): use phase to manage express middleware
- Loading branch information
1 parent
00d0138
commit 6c1be3a
Showing
3 changed files
with
259 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
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,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
126
packages/rest/test/integration/middleware-phase.integration.ts
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,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}); | ||
}; | ||
} |