From 03fd4b8c024cd155be947103f1102a0d294fbbd3 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Sun, 7 Apr 2019 09:37:59 -0700 Subject: [PATCH] feat(context): add support for global interceptors --- docs/site/Interceptors.md | 38 ++++++-- .../acceptance/interceptor.acceptance.ts | 86 +++++++++++++++++++ packages/context/src/interceptor.ts | 24 ++++++ packages/context/src/keys.ts | 5 ++ 4 files changed, 146 insertions(+), 7 deletions(-) diff --git a/docs/site/Interceptors.md b/docs/site/Interceptors.md index db1f77692781..23af2d36c5b4 100644 --- a/docs/site/Interceptors.md +++ b/docs/site/Interceptors.md @@ -147,7 +147,7 @@ Interceptors form a cascading chain of handlers around the target method invocation. We can apply interceptors by decorating methods/classes with `@intercept`. -## @intercept +### @intercept Syntax: `@intercept(...interceptorFunctionsOrBindingKeys)` @@ -257,7 +257,8 @@ class MyControllerWithClassLevelInterceptors { return `Hello, ${name}`; } - @intercept(log, logSync) + @intercept(log) + @intercept(logSync) greetSync(name: string) { return `Hello, ${name}`; } @@ -269,12 +270,35 @@ class MyControllerWithClassLevelInterceptors { } ``` -Here is the list of interceptors invoked for each method: +### Global interceptors + +Global interceptors are discovered from the `InvocationContext`. They are +registered as bindings with `interceptor` tag. For example, + +```ts +app + .bind('interceptors.MetricsInterceptor') + .toProvider(MetricsInterceptorProvider) + .tag('interceptor'); +``` + +### Order of invocation for interceptors + +Multiple `@intercept` decorators can be applied to a class or a method. The +order of invocation is determined by how `@intercept` is specified. The list of +interceptors is created from top to bottom and from left to right. Duplicate +entries are removed from their first occurrences. + +Here is the list of interceptors invoked for each method on +`MyControllerWithClassLevelInterceptors`. -- greetStatic: `log` -- greetStaticWithDI: `log` -- greetSync: `log`, `logSync` -- greet: `convertName`, `log` +- greetStatic: `log` (from the class) +- greetStaticWithDI: `log` (from the method, class level `log` is removed) +- greetSync: `log`, `logSync` (from the method, method level `log` is preserved + and class level `log` is removed) +- greet: `convertName`, `log` (from the method, method level `log` is preserved + and class level `log` is removed. This allows a method to explicitly control + the order.) ## Invoke a method with interceptors diff --git a/packages/context/src/__tests__/acceptance/interceptor.acceptance.ts b/packages/context/src/__tests__/acceptance/interceptor.acceptance.ts index 0176d2538e06..b6aaf30ea30e 100644 --- a/packages/context/src/__tests__/acceptance/interceptor.acceptance.ts +++ b/packages/context/src/__tests__/acceptance/interceptor.acceptance.ts @@ -6,6 +6,7 @@ import {expect} from '@loopback/testlab'; import { Context, + ContextTags, inject, intercept, Interceptor, @@ -218,6 +219,91 @@ describe('Interceptor', () => { }); }); + context('global interceptors', () => { + beforeEach(givenGlobalInterceptor); + + it('invokes sync and async interceptors', async () => { + const msg = await invokeMethodWithInterceptors( + ctx, + controllerWithClassInterceptors, + 'greetSync', + ['John'], + ); + expect(msg).to.equal('Hello, John'); + expect(events).to.eql([ + 'globalLog: before-greetSync', + 'log: before-greetSync', + 'logSync: before-greetSync', + 'logSync: after-greetSync', + 'log: after-greetSync', + 'globalLog: after-greetSync', + ]); + }); + + it('invokes async interceptors on an async method', async () => { + const msg = await invokeMethodWithInterceptors( + ctx, + controllerWithClassInterceptors, + 'greet', + ['John'], + ); + expect(msg).to.equal('Hello, JOHN'); + expect(events).to.eql([ + 'globalLog: before-greet', + 'convertName: before-greet', + 'log: before-greet', + 'log: after-greet', + 'convertName: after-greet', + 'globalLog: after-greet', + ]); + }); + + it('invokes interceptors on a static method', async () => { + const msg = await invokeMethodWithInterceptors( + ctx, + MyControllerWithClassLevelInterceptors, + 'greetStatic', + ['John'], + ); + expect(msg).to.equal('Hello, John'); + expect(events).to.eql([ + 'globalLog: before-greetStatic', + 'log: before-greetStatic', + 'log: after-greetStatic', + 'globalLog: after-greetStatic', + ]); + }); + + it('invokes interceptors on a static method with DI', async () => { + ctx.bind('name').to('John'); + const msg = await invokeMethod( + MyControllerWithClassLevelInterceptors, + 'greetStaticWithDI', + ctx, + ); + expect(msg).to.equal('Hello, John'); + expect(events).to.eql([ + 'globalLog: before-greetStaticWithDI', + 'log: before-greetStaticWithDI', + 'log: after-greetStaticWithDI', + 'globalLog: after-greetStaticWithDI', + ]); + }); + + function givenGlobalInterceptor() { + const globalLog: Interceptor = async (invocationCtx, next) => { + events.push('globalLog: before-' + invocationCtx.methodName); + const result = await next(); + events.push('globalLog: after-' + invocationCtx.methodName); + return result; + }; + ctx + .bind('globalLog') + .to(globalLog) + .tag(ContextTags.INTERCEPTOR); + } + }); + let events: string[]; const logSync: Interceptor = (invocationCtx, next) => { diff --git a/packages/context/src/interceptor.ts b/packages/context/src/interceptor.ts index 7acc33152f7d..2b94355fc70d 100644 --- a/packages/context/src/interceptor.ts +++ b/packages/context/src/interceptor.ts @@ -13,8 +13,11 @@ import { } from '@loopback/metadata'; import * as assert from 'assert'; import * as debugFactory from 'debug'; +import {filterByTag} from './binding-filter'; import {BindingAddress} from './binding-key'; import {Context} from './context'; +import {ContextView} from './context-view'; +import {ContextTags} from './keys'; import {transformValueOrPromise, ValueOrPromise} from './value-promise'; const debug = debugFactory('loopback:context:intercept'); const getTargetName = DecoratorFactory.getTargetName; @@ -42,6 +45,22 @@ export class InvocationContext extends Context { ) { super(parent); } + + getGlobalInterceptors() { + const view = new ContextView( + this, + filterByTag(ContextTags.INTERCEPTOR), + ); + return view.values(); + } + + getGlobalInterceptorKeys() { + const view = new ContextView( + this, + filterByTag(ContextTags.INTERCEPTOR), + ); + return view.bindings.map(b => b.key); + } } /** @@ -214,6 +233,11 @@ export function invokeMethodWithInterceptors( // Inserting class level interceptors before method level ones interceptors = mergeInterceptors(classInterceptors, interceptors); + const globalInterceptors = invocationCtx.getGlobalInterceptorKeys(); + + // Inserting global interceptors + interceptors = mergeInterceptors(globalInterceptors, interceptors); + return invokeInterceptors(invocationCtx, interceptors); } diff --git a/packages/context/src/keys.ts b/packages/context/src/keys.ts index 2ab2e3a6fc25..8d4c28199046 100644 --- a/packages/context/src/keys.ts +++ b/packages/context/src/keys.ts @@ -23,4 +23,9 @@ export namespace ContextTags { * Binding key for the artifact */ export const KEY = 'key'; + + /** + * Binding tag for global interceptors + */ + export const INTERCEPTOR = 'interceptor'; }