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 009cbc0
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 9 deletions.
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);
}
}
}
});
18 changes: 15 additions & 3 deletions packages/rest/src/__tests__/acceptance/middleware/test-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,11 @@ export class TestHelper {
}

async testSpyLog(spyBinding: Binding<unknown>) {
this.app.configure<SpyConfig>(spyBinding.key).to({action: 'log'});
// We have to re-configure at restServer level
// as `this.app.middleware()` delegates to `restServer`
this.app.restServer
.configure<SpyConfig>(spyBinding.key)
.to({action: 'log'});

await this.client
.post('/hello')
Expand All @@ -76,7 +80,11 @@ export class TestHelper {
}

async testSpyMock(spyBinding: Binding<unknown>) {
this.app.configure<SpyConfig>(spyBinding.key).to({action: 'mock'});
// We have to re-configure at restServer level
// as `this.app.middleware()` delegates to `restServer`
this.app.restServer
.configure<SpyConfig>(spyBinding.key)
.to({action: 'mock'});
await this.client
.post('/hello')
.send('"World"')
Expand All @@ -86,7 +94,11 @@ export class TestHelper {
}

async testSpyReject(spyBinding: Binding<unknown>) {
this.app.configure<SpyConfig>(spyBinding.key).to({action: 'reject'});
// We have to re-configure at restServer level
// as `this.app.middleware()` delegates to `restServer`
this.app.restServer
.configure<SpyConfig>(spyBinding.key)
.to({action: 'reject'});
await this.client
.post('/hello')
.send('"World"')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const debug = debugFactory('loopback:rest:middleware:spy');
const spyMiddlewareFactory: MiddlewareFactory<SpyConfig> = config => {
const options = {...config};
return function spy(req, res, next) {
debug('config', options);
switch (options?.action) {
case 'mock':
debug('spy - MOCK');
Expand Down
30 changes: 25 additions & 5 deletions packages/rest/src/middleware/middleware-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
extensionPoint,
} from '@loopback/core';
import assert from 'assert';
import debugFactory from 'debug';
import {RequestContext} from '../request-context';
import {
createInterceptor,
Expand All @@ -32,6 +33,8 @@ import {
RequestInterceptor,
} from './types';

const debug = debugFactory('loopback:rest:middleware');

/**
* Default extension point name for middleware
*/
Expand All @@ -53,13 +56,30 @@ export class MiddlewareProvider implements Provider<Middleware> {
}

async action(requestCtx: RequestContext) {
debug('Binding', this.binding);
debug(
'Get middleware for %s %s',
requestCtx.request.method,
requestCtx.request.originalUrl,
);
const extensionPointName =
this.binding?.tagMap[CoreTags.EXTENSION_POINT] ??
MIDDLEWARE_EXTENSION_POINT;
// Find extensions for the given extension point binding
const filter = extensionFilter(extensionPointName);
if (debug.enabled) {
debug(
'Middleware for extension point %s:',
extensionPointName,
requestCtx.find(filter).map(b => ({
key: b.key,
configValue: requestCtx.getConfigSync(b.key),
})),
);
}
const chain = new GenericInterceptorChain<RequestContext>(
requestCtx,
// Find extensions for the given extension point binding
extensionFilter(
this.binding?.tagMap[CoreTags.EXTENSION_POINT] ??
MIDDLEWARE_EXTENSION_POINT,
),
filter,
compareBindingsByTag('group'),
);
return chain.invokeInterceptors();
Expand Down
26 changes: 26 additions & 0 deletions packages/rest/src/rest.application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import {ServeStaticOptions} from 'serve-static';
import {format} from 'util';
import {BodyParser} from './body-parsers';
import {RestBindings} from './keys';
import {
MiddlewareActionBindingOptions,
MiddlewareFactory,
RequestInterceptor,
} from './middleware';
import {RestComponent} from './rest.component';
import {HttpRequestListener, HttpServerLike, RestServer} from './rest.server';
import {
Expand Down Expand Up @@ -133,6 +138,27 @@ export class RestApplication extends Application implements HttpServerLike {
this.restServer.basePath(path);
}

/**
* Bind a middleware interceptor to this server context
*
* @example
* ```ts
* app.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> {
return this.restServer.middleware(middleware, middlewareConfig, options);
}

/**
* Register a new Controller-based route.
*
Expand Down
27 changes: 27 additions & 0 deletions packages/rest/src/rest.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ import {writeErrorToResponse} from 'strong-error-handler';
import {BodyParser, REQUEST_BODY_PARSER_TAG} from './body-parsers';
import {HttpHandler} from './http-handler';
import {RestBindings, RestTags} from './keys';
import {
MiddlewareActionBindingOptions,
MiddlewareFactory,
registerMiddleware,
RequestInterceptor,
} from './middleware';
import {RequestContext} from './request-context';
import {
ControllerClass,
Expand Down Expand Up @@ -578,6 +584,27 @@ export class RestServer extends Context implements Server, HttpServerLike {
);
}

/**
* 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> {
return registerMiddleware(this, middleware, middlewareConfig, options);
}

/**
* Register a new Controller-based route.
*
Expand Down

0 comments on commit 009cbc0

Please sign in to comment.