-
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): add middleware() to RestServer/RestApplication classes
- Loading branch information
1 parent
75b32a2
commit a5911ca
Showing
9 changed files
with
463 additions
and
9 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,229 @@ | ||
--- | ||
lang: en | ||
title: 'Use Express middleware' | ||
keywords: LoopBack 4.0, LoopBack 4, Express, Middleware | ||
sidebar: lb4_sidebar | ||
permalink: /doc/en/lb4/Express-middleware.html | ||
--- | ||
|
||
## Overview | ||
|
||
Express is the most popular web framework for Node.js developers. Middleware is | ||
the basic building blocks for Express applications. The following is quoted from | ||
[Express web site](https://expressjs.com/en/guide/using-middleware.html): | ||
|
||
> Express is a routing and middleware web framework that has minimal | ||
> functionality of its own: An Express application is essentially a series of | ||
> middleware function calls. | ||
LookBack 4 leverages Express behind the scene for its REST server | ||
implementation. We haven't exposed middleware capabilities to users before an | ||
elegant way can be crafted to nicely fit Express middleware into the LoopBack 4 | ||
programming model. We believe that we now have a good middle-ground to combine | ||
the best of breeds of both projects. | ||
|
||
The integration is built on top of the [interceptor](Interceptors.md) support as | ||
follows: | ||
|
||
We create a few helper functions that wraps an Express middleware module into an | ||
LoopBack 4 interceptor. | ||
|
||
### Create an interceptor from Express middleware module name or factory function | ||
|
||
```ts | ||
/** | ||
* Create an interceptor function from express middleware | ||
* @param middleware - Express middleware module name or factory function | ||
* @param middlewareConfig - Configuration for the middleware | ||
* | ||
* @typeParam CFG - Configuration type | ||
* @typeParam CTX - Context type | ||
*/ | ||
export function createInterceptor<CFG, CTX extends Context = InvocationContext>( | ||
middleware: string | MiddlewareFactory<CFG>, | ||
middlewareConfig?: CFG, | ||
): GenericInterceptor<CTX> { | ||
// ... | ||
} | ||
``` | ||
|
||
### Adapt an Express middleware handler function to an interceptor | ||
|
||
```ts | ||
/** | ||
* Wrap an express middleware handler function as an interceptor | ||
* @param handlerFn - Express middleware handler function | ||
* | ||
* @typeParam CTX - Context type | ||
*/ | ||
export function toInterceptor<CTX extends Context = InvocationContext>( | ||
handlerFn: ExpressRequestHandler, | ||
): GenericInterceptor<CTX> { | ||
// ... | ||
} | ||
``` | ||
|
||
### Define a provider class to allow dependency injection of middleware configuration | ||
|
||
```ts | ||
/** | ||
* Define a provider class that wraps the middleware as an interceptor | ||
* @param middleware - Middleware module name or factory function | ||
* @param className - Class name for the generated provider class | ||
* | ||
* @typeParam CFG - Configuration type | ||
* @typeParam CTX - Context type | ||
*/ | ||
export function defineInterceptorProvider< | ||
CFG, | ||
CTX extends Context = InvocationContext | ||
>( | ||
middleware: string | MiddlewareFactory<CFG>, | ||
className?: string, | ||
): Constructor<Provider<GenericInterceptor<CTX>>> { | ||
// ... | ||
} | ||
``` | ||
|
||
Alternatively, we can create a subclass of `MiddlewareInterceptorProvider`. | ||
|
||
### Use middleware as invocation interceptors | ||
|
||
With the ability to wrap Express middleware as LoopBack 4 interceptors, we can | ||
use the same programming model to register middleware as global interceptors or | ||
local interceptors denoted by `@intercept` decorators at class and method | ||
levels. | ||
|
||
### Introduce middleware to REST sequence of actions | ||
|
||
There are a few steps involved in the default sequence of actions. See | ||
`Sequence.md` for more details. | ||
|
||
It's often desirable to reuse Express middleware in the sequence to handle API | ||
requests/responses without reinventing the world. We now add an `Middleware` | ||
action as the first step in the default sequence. The action itself is an | ||
interceptor chain of `RequestContext`. It uses the powerful | ||
[extension point/extension pattern](Extension-point-and-extensions.md) to | ||
contribute and discover registered Express middleware. | ||
|
||
#### Register an Express middleware for the sequence | ||
|
||
````ts | ||
/** | ||
* Bind a middleware interceptor to this server context | ||
* | ||
* @example | ||
* ```ts | ||
* server.middleware('express-middleware-name', {}); | ||
* ``` | ||
* @param middleware - Middleware module name or factory function | ||
* @param middlewareConfig - Middleware config | ||
* @param options - Options for registration | ||
* | ||
* @typeParam CFG - Configuration type | ||
*/ | ||
middleware<CFG>( | ||
middleware: string | MiddlewareFactory<CFG>, | ||
middlewareConfig?: CFG, | ||
options: MiddlewareActionBindingOptions = {}, | ||
): Binding<RequestInterceptor> { | ||
// ... | ||
} | ||
```` | ||
|
||
```ts | ||
const binding = app.middleware(spy, spyConfig, { | ||
injectConfiguration: true, | ||
key: 'interceptors.middleware.spy', | ||
}); | ||
``` | ||
|
||
#### Default sequence | ||
|
||
We add a new `middleware` property to `DefaultSequence` to allow injection of | ||
the middleware action while keeping backward compatibility. | ||
|
||
```ts | ||
export class DefaultSequence implements SequenceHandler { | ||
/** | ||
* Optional middleware chain | ||
* Invokes registered middleware (injected via SequenceActions.MIDDLEWARE). | ||
*/ | ||
@inject(SequenceActions.MIDDLEWARE, {optional: true}) | ||
protected middleware?: Middleware; | ||
|
||
// ... | ||
async handle(context: RequestContext): Promise<void> { | ||
try { | ||
const {request, response} = context; | ||
if (this.middleware != null) { | ||
await this.middleware(context); | ||
} | ||
const route = this.findRoute(request); | ||
const args = await this.parseParams(request, route); | ||
const result = await this.invoke(route, args); | ||
|
||
debug('%s result -', route.describe(), result); | ||
this.send(response, result); | ||
} catch (error) { | ||
this.reject(context, error); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
#### Extend sequence with more than one middleware steps | ||
|
||
Sometimes we want to add middleware between built-in steps, for example, do some | ||
post-processing before the result is sent to the HTTP response. | ||
|
||
```ts | ||
class SequenceWithMiddleware extends DefaultSequence { | ||
/** | ||
* Optional middleware chain | ||
* Invokes registered middleware (injected via SequenceActions.MIDDLEWARE). | ||
*/ | ||
@inject('middleware.afterInvoke', {optional: true}) | ||
protected middlewareAfterInvoke?: Middleware; | ||
async handle(context: RequestContext): Promise<void> { | ||
try { | ||
const {request, response} = context; | ||
if (this.middleware != null) { | ||
await this.middleware(context); | ||
} | ||
const route = this.findRoute(request); | ||
const args = await this.parseParams(request, route); | ||
const result = await this.invoke(route, args); | ||
// Invoke middleware after invoke | ||
context.bind('invocation.result').to(result); | ||
if (this.middlewareAfterInvoke != null) { | ||
await this.middlewareAfterInvoke(context); | ||
} | ||
this.send(response, result); | ||
} catch (error) { | ||
this.reject(context, error); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Now we can set up applications to leverage the new sequence: | ||
|
||
```ts | ||
// Create another middleware phase | ||
app | ||
.bind('middleware.afterInvoke') | ||
.toProvider(MiddlewareProvider) | ||
.tag({[CoreTags.EXTENSION_POINT]: POST_INVOCATION_MIDDLEWARE}); | ||
helper.app.sequence(SequenceWithMiddleware); | ||
|
||
const secondSpy = app | ||
.middleware(spy, undefined, { | ||
key: 'middleware.secondSpy', | ||
extensionPointName: POST_INVOCATION_MIDDLEWARE, | ||
}) | ||
// Set the scope to be `TRANSIENT` so that the new config can be loaded | ||
.inScope(BindingScope.TRANSIENT); | ||
|
||
app.restServer.configure<SpyConfig>(secondSpy.key).to({action: 'log'}); | ||
``` |
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
51 changes: 51 additions & 0 deletions
51
packages/rest/src/__tests__/acceptance/middleware/middleware-registeration.acceptance.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,51 @@ | ||
// Copyright IBM Corp. 2020. 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 {expect} from '@loopback/testlab'; | ||
import {spy, SPY, SpyConfig, TestFunction, TestHelper} from './test-helpers'; | ||
|
||
describe('app.middleware()', () => { | ||
let helper: TestHelper; | ||
|
||
function runTests(action: 'log' | 'mock' | 'reject', testFn: TestFunction) { | ||
describe(`app.middleware - ${action}`, () => { | ||
const spyConfig: SpyConfig = {action}; | ||
beforeEach(givenTestApp); | ||
afterEach(() => helper?.stop()); | ||
|
||
it('registers a middleware interceptor provider class by factory', () => { | ||
const binding = helper.app.middleware(spy, spyConfig); | ||
return testFn(binding); | ||
}); | ||
|
||
it('registers a middleware interceptor as handler function', () => { | ||
const binding = helper.app.restServer.middleware(spy, spyConfig, { | ||
injectConfiguration: false, | ||
key: 'interceptors.middleware.spy', | ||
}); | ||
expect(binding.key).to.eql('interceptors.middleware.spy'); | ||
return testFn(binding); | ||
}); | ||
|
||
it('registers a middleware interceptor provider class by module name', () => { | ||
const binding = helper.app.middleware(SPY, spyConfig, { | ||
className: 'spy', | ||
}); | ||
expect(binding.key).to.eql('middleware.spy'); | ||
return testFn(binding); | ||
}); | ||
}); | ||
} | ||
|
||
runTests('log', binding => helper.testSpyLog(binding)); | ||
runTests('mock', binding => helper.testSpyMock(binding)); | ||
runTests('reject', binding => helper.testSpyReject(binding)); | ||
|
||
function givenTestApp() { | ||
helper = new TestHelper(); | ||
helper.bindController(); | ||
return helper.start(); | ||
} | ||
}); |
89 changes: 89 additions & 0 deletions
89
packages/rest/src/__tests__/acceptance/middleware/middleware-sequence.acceptance.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,89 @@ | ||
// Copyright IBM Corp. 2020. 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 {BindingScope, CoreTags, inject} from '@loopback/core'; | ||
import {Middleware, MiddlewareProvider} from '../../../middleware'; | ||
import {RequestContext} from '../../../request-context'; | ||
import {DefaultSequence} from '../../../sequence'; | ||
import {spy, TestHelper} from './test-helpers'; | ||
|
||
const POST_INVOCATION_MIDDLEWARE = 'middleware.postInvocation'; | ||
describe('Middleware in sequence', () => { | ||
let helper: TestHelper; | ||
|
||
beforeEach(givenTestApp); | ||
afterEach(() => helper?.stop()); | ||
|
||
it('registers a middleware in default slot', () => { | ||
const binding = helper.app.middleware(spy, undefined); | ||
return helper.testSpyLog(binding); | ||
}); | ||
|
||
it('registers a middleware in afterInvoke slot', () => { | ||
const binding = helper.app.middleware(spy, undefined, { | ||
extensionPointName: POST_INVOCATION_MIDDLEWARE, | ||
}); | ||
return helper.testSpyLog(binding); | ||
}); | ||
|
||
it('registers a middleware in both slots', async () => { | ||
const firstSpy = helper.app.middleware(spy, undefined, { | ||
key: 'middleware.firstSpy', | ||
}); | ||
const secondSpy = helper.app | ||
.middleware(spy, undefined, { | ||
key: 'middleware.secondSpy', | ||
extensionPointName: POST_INVOCATION_MIDDLEWARE, | ||
}) | ||
// Set the scope to be `TRANSIENT` so that the new config can be loaded | ||
.inScope(BindingScope.TRANSIENT); | ||
await helper.testSpyLog(firstSpy); | ||
await helper.testSpyReject(secondSpy); | ||
}); | ||
|
||
function givenTestApp() { | ||
helper = new TestHelper(); | ||
// Create another middleware phase | ||
helper.app | ||
.bind('middleware.afterInvoke') | ||
.toProvider(MiddlewareProvider) | ||
.tag({[CoreTags.EXTENSION_POINT]: POST_INVOCATION_MIDDLEWARE}); | ||
helper.app.sequence(SequenceWithMiddleware); | ||
helper.bindController(); | ||
return helper.start(); | ||
} | ||
|
||
class SequenceWithMiddleware extends DefaultSequence { | ||
/** | ||
* Optional middleware chain | ||
* Invokes registered middleware (injected via SequenceActions.MIDDLEWARE). | ||
*/ | ||
@inject('middleware.afterInvoke', {optional: true}) | ||
protected middlewareAfterInvoke?: Middleware; | ||
|
||
async handle(context: RequestContext): Promise<void> { | ||
try { | ||
const {request, response} = context; | ||
if (this.middleware != null) { | ||
await this.middleware(context); | ||
} | ||
const route = this.findRoute(request); | ||
const args = await this.parseParams(request, route); | ||
|
||
const result = await this.invoke(route, args); | ||
|
||
// Invoke middleware after invoke | ||
context.bind('invocation.result').to(result); | ||
if (this.middlewareAfterInvoke != null) { | ||
await this.middlewareAfterInvoke(context); | ||
} | ||
|
||
this.send(response, result); | ||
} catch (error) { | ||
this.reject(context, error); | ||
} | ||
} | ||
} | ||
}); |
Oops, something went wrong.