diff --git a/docs/site/Interceptors.md b/docs/site/Interceptors.md index 589f44d304a7..1ce48b43baad 100644 --- a/docs/site/Interceptors.md +++ b/docs/site/Interceptors.md @@ -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 { + // ... +} +``` + ### Logic around `next` An interceptor will receive the `next` parameter, which is a function to execute diff --git a/packages/context/src/__tests__/unit/interceptor.unit.ts b/packages/context/src/__tests__/unit/interceptor.unit.ts index 896ff608376b..a44bf62910ff 100644 --- a/packages/context/src/__tests__/unit/interceptor.unit.ts +++ b/packages/context/src/__tests__/unit/interceptor.unit.ts @@ -15,6 +15,7 @@ import { InterceptedInvocationContext, Interceptor, InterceptorOrKey, + InvocationSource, mergeInterceptors, Provider, } from '../..'; @@ -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}`; @@ -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 | undefined = undefined; + if (source != null) { + invocationSource = { + type: source, + value: source, + }; + } + return new InterceptedInvocationContext( + ctx, + new MyController(), + 'greet', + ['John'], + invocationSource, + ); } }); diff --git a/packages/context/src/interceptor.ts b/packages/context/src/interceptor.ts index 94f37dec4a48..aa5fc36b49a8 100644 --- a/packages/context/src/interceptor.ts +++ b/packages/context/src/interceptor.ts @@ -48,7 +48,10 @@ export class InterceptedInvocationContext extends InvocationContext { */ getGlobalInterceptorBindingKeys(): string[] { const bindings: Readonly>[] = 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); @@ -56,6 +59,27 @@ export class InterceptedInvocationContext extends InvocationContext { return keys; } + /** + * Check if the binding for a global interceptor matches the source type + * of the invocation + * @param binding - Binding + */ + private applicableTo(binding: Readonly>) { + 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 diff --git a/packages/context/src/keys.ts b/packages/context/src/keys.ts index 7e0e014c34b3..3ded8c77aae1 100644 --- a/packages/context/src/keys.ts +++ b/packages/context/src/keys.ts @@ -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 */