Skip to content

Commit

Permalink
feat(rest): add middleware() to RestServer/RestApplication classes
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Apr 16, 2020
1 parent 75b32a2 commit a5911ca
Show file tree
Hide file tree
Showing 9 changed files with 463 additions and 9 deletions.
229 changes: 229 additions & 0 deletions docs/site/Express-middleware.md
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'});
```
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ describe('Middleware request interceptor', () => {
runTests('mock', binding => helper.testSpyMock(binding));
runTests('reject', binding => helper.testSpyReject(binding));


function givenTestApp() {
helper = new TestHelper();
helper.bindController();
Expand Down
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();
}
});
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);
}
}
}
});
Loading

0 comments on commit a5911ca

Please sign in to comment.