Skip to content

Commit

Permalink
feat(context): add support for global interceptors
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Apr 7, 2019
1 parent 3859148 commit 03fd4b8
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 7 deletions.
38 changes: 31 additions & 7 deletions docs/site/Interceptors.md
Expand Up @@ -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)`

Expand Down Expand Up @@ -257,7 +257,8 @@ class MyControllerWithClassLevelInterceptors {
return `Hello, ${name}`;
}

@intercept(log, logSync)
@intercept(log)
@intercept(logSync)
greetSync(name: string) {
return `Hello, ${name}`;
}
Expand All @@ -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

Expand Down
Expand Up @@ -6,6 +6,7 @@
import {expect} from '@loopback/testlab';
import {
Context,
ContextTags,
inject,
intercept,
Interceptor,
Expand Down Expand Up @@ -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) => {
Expand Down
24 changes: 24 additions & 0 deletions packages/context/src/interceptor.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -42,6 +45,22 @@ export class InvocationContext extends Context {
) {
super(parent);
}

getGlobalInterceptors() {
const view = new ContextView<Interceptor>(
this,
filterByTag(ContextTags.INTERCEPTOR),
);
return view.values();
}

getGlobalInterceptorKeys() {
const view = new ContextView<Interceptor>(
this,
filterByTag(ContextTags.INTERCEPTOR),
);
return view.bindings.map(b => b.key);
}
}

/**
Expand Down Expand Up @@ -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);
}

Expand Down
5 changes: 5 additions & 0 deletions packages/context/src/keys.ts
Expand Up @@ -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';
}

0 comments on commit 03fd4b8

Please sign in to comment.