Skip to content

Commit

Permalink
feat(context): allow global interceptors to be applied based on sourc…
Browse files Browse the repository at this point in the history
…e types
  • Loading branch information
raymondfeng committed Dec 3, 2019
1 parent 2a1ccb4 commit 77cbd01
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 5 deletions.
26 changes: 26 additions & 0 deletions docs/site/Interceptors.md
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,32 @@ The implementation of an interceptor can check `source` to decide if its logic
should apply. For example, a global interceptor that provides caching for REST
APIs should only run if the source is from a REST Route.

A global interceptor can also be tagged with
`ContextTags.GLOBAL_INTERCEPTOR_SOURCE` using a value of string or string array
to indicate if it should be applied to source types of invocations. If the tag
is not present, the interceptor applies to invocations of any source type. For
example:

```ts
ctx
.bind('globalInterceptors.authInterceptor')
.to(authInterceptor)
.apply(asGlobalInterceptor('auth'))
// Do not apply for `proxy` source type
.tag({[ContextTags.GLOBAL_INTERCEPTOR_SOURCE]: 'route'});
```

The tag can also be declared for the provider class.

```ts
@globalInterceptor('log', {
tags: {[ContextTags.GLOBAL_INTERCEPTOR_SOURCE]: ['proxy']},
})
export class LogInterceptor implements Provider<Interceptor> {
// ...
}
```

### Logic around `next`

An interceptor will receive the `next` parameter, which is a function to execute
Expand Down
82 changes: 78 additions & 4 deletions packages/context/src/__tests__/unit/interceptor.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
InterceptedInvocationContext,
Interceptor,
InterceptorOrKey,
InvocationSource,
mergeInterceptors,
Provider,
} from '../..';
Expand Down Expand Up @@ -190,6 +191,68 @@ describe('globalInterceptors', () => {
});
});

it('includes interceptors that match the source type', () => {
ctx
.bind('globalInterceptors.authInterceptor')
.to(authInterceptor)
.apply(asGlobalInterceptor('auth'))
// Allows `route` source type explicitly
.tag({[ContextTags.GLOBAL_INTERCEPTOR_SOURCE]: 'route'});

ctx
.bind('globalInterceptors.logInterceptor')
.to(logInterceptor)
.apply(asGlobalInterceptor('log'));
// No source type is tagged - always apply

const invocationCtx = givenInvocationContext('route');

const keys = invocationCtx.getGlobalInterceptorBindingKeys();
expect(keys).to.eql([
'globalInterceptors.authInterceptor',
'globalInterceptors.logInterceptor',
]);
});

it('excludes interceptors that do not match the source type', () => {
ctx
.bind('globalInterceptors.authInterceptor')
.to(authInterceptor)
.apply(asGlobalInterceptor('auth'))
// Do not apply for `proxy` source type
.tag({[ContextTags.GLOBAL_INTERCEPTOR_SOURCE]: 'route'});

ctx
.bind('globalInterceptors.logInterceptor')
.to(logInterceptor)
.apply(asGlobalInterceptor('log'));

const invocationCtx = givenInvocationContext('proxy');

const keys = invocationCtx.getGlobalInterceptorBindingKeys();
expect(keys).to.eql(['globalInterceptors.logInterceptor']);
});

it('excludes interceptors that do not match the source type - with array', () => {
ctx
.bind('globalInterceptors.authInterceptor')
.to(authInterceptor)
.apply(asGlobalInterceptor('auth'))
// Do not apply for `proxy` source type
.tag({[ContextTags.GLOBAL_INTERCEPTOR_SOURCE]: 'route'});

ctx
.bind('globalInterceptors.logInterceptor')
.to(logInterceptor)
.apply(asGlobalInterceptor('log'))
.tag({[ContextTags.GLOBAL_INTERCEPTOR_SOURCE]: ['route', 'proxy']});

const invocationCtx = givenInvocationContext('proxy');

const keys = invocationCtx.getGlobalInterceptorBindingKeys();
expect(keys).to.eql(['globalInterceptors.logInterceptor']);
});

class MyController {
greet(name: string) {
return `Hello, ${name}`;
Expand All @@ -200,9 +263,20 @@ describe('globalInterceptors', () => {
ctx = new Context();
}

function givenInvocationContext() {
return new InterceptedInvocationContext(ctx, new MyController(), 'greet', [
'John',
]);
function givenInvocationContext(source?: string) {
let invocationSource: InvocationSource<string> | undefined = undefined;
if (source != null) {
invocationSource = {
type: source,
value: source,
};
}
return new InterceptedInvocationContext(
ctx,
new MyController(),
'greet',
['John'],
invocationSource,
);
}
});
26 changes: 25 additions & 1 deletion packages/context/src/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,38 @@ export class InterceptedInvocationContext extends InvocationContext {
*/
getGlobalInterceptorBindingKeys(): string[] {
const bindings: Readonly<Binding<Interceptor>>[] = this.find(
filterByTag(ContextTags.GLOBAL_INTERCEPTOR),
binding =>
filterByTag(ContextTags.GLOBAL_INTERCEPTOR)(binding) &&
// Only include interceptors that match the source type of the invocation
this.applicableTo(binding),
);
this.sortGlobalInterceptorBindings(bindings);
const keys = bindings.map(b => b.key);
debug('Global interceptor binding keys:', keys);
return keys;
}

/**
* Check if the binding for a global interceptor matches the source type
* of the invocation
* @param binding - Binding
*/
private applicableTo(binding: Readonly<Binding<unknown>>) {
const sourceType = this.source?.type;
// Unknown source type, always apply
if (sourceType == null) return true;
const allowedSource: string | string[] =
binding.tagMap[ContextTags.GLOBAL_INTERCEPTOR_SOURCE];
return (
// No tag, always apply
allowedSource == null ||
// source matched
allowedSource === sourceType ||
// source included in the string[]
(Array.isArray(allowedSource) && allowedSource.includes(sourceType))
);
}

/**
* Sort global interceptor bindings by `globalInterceptorGroup` tags
* @param bindings - An array of global interceptor bindings
Expand Down
7 changes: 7 additions & 0 deletions packages/context/src/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ export namespace ContextTags {
*/
export const GLOBAL_INTERCEPTOR = 'globalInterceptor';

/**
* Binding tag for global interceptors to specify sources of invocations that
* the interceptor should apply. The tag value can be a string or string[], such
* as `'route'` or `['route', 'proxy']`.
*/
export const GLOBAL_INTERCEPTOR_SOURCE = 'globalInterceptorSource';

/**
* Binding tag for group name of global interceptors
*/
Expand Down

0 comments on commit 77cbd01

Please sign in to comment.