@loopback/context 1.23.5 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/binding-config.js +1 -1
  3. package/dist/binding-config.js.map +1 -1
  4. package/dist/binding-filter.d.ts +19 -1
  5. package/dist/binding-filter.js +40 -7
  6. package/dist/binding-filter.js.map +1 -1
  7. package/dist/binding-inspector.js +12 -11
  8. package/dist/binding-inspector.js.map +1 -1
  9. package/dist/binding-sorter.js +2 -2
  10. package/dist/binding-sorter.js.map +1 -1
  11. package/dist/binding.d.ts +42 -4
  12. package/dist/binding.js +40 -10
  13. package/dist/binding.js.map +1 -1
  14. package/dist/context-event.d.ts +23 -0
  15. package/dist/context-event.js +7 -0
  16. package/dist/context-event.js.map +1 -0
  17. package/dist/context-observer.d.ts +1 -36
  18. package/dist/context-subscription.d.ts +147 -0
  19. package/dist/context-subscription.js +336 -0
  20. package/dist/context-subscription.js.map +1 -0
  21. package/dist/context-tag-indexer.d.ts +42 -0
  22. package/dist/context-tag-indexer.js +134 -0
  23. package/dist/context-tag-indexer.js.map +1 -0
  24. package/dist/context-view.d.ts +2 -1
  25. package/dist/context-view.js +5 -2
  26. package/dist/context-view.js.map +1 -1
  27. package/dist/context.d.ts +35 -66
  28. package/dist/context.js +78 -250
  29. package/dist/context.js.map +1 -1
  30. package/dist/index.d.ts +5 -3
  31. package/dist/index.js +4 -3
  32. package/dist/index.js.map +1 -1
  33. package/dist/inject-config.js +3 -3
  34. package/dist/inject-config.js.map +1 -1
  35. package/dist/inject.d.ts +2 -2
  36. package/dist/inject.js +18 -11
  37. package/dist/inject.js.map +1 -1
  38. package/dist/interception-proxy.d.ts +15 -3
  39. package/dist/interception-proxy.js +20 -4
  40. package/dist/interception-proxy.js.map +1 -1
  41. package/dist/interceptor-chain.js +5 -2
  42. package/dist/interceptor-chain.js.map +1 -1
  43. package/dist/interceptor.d.ts +6 -0
  44. package/dist/interceptor.js +38 -12
  45. package/dist/interceptor.js.map +1 -1
  46. package/dist/invocation.d.ts +20 -2
  47. package/dist/invocation.js +14 -12
  48. package/dist/invocation.js.map +1 -1
  49. package/dist/keys.d.ts +6 -0
  50. package/dist/keys.js +6 -0
  51. package/dist/keys.js.map +1 -1
  52. package/dist/resolution-session.d.ts +1 -0
  53. package/dist/resolution-session.js +13 -6
  54. package/dist/resolution-session.js.map +1 -1
  55. package/dist/resolver.js +13 -8
  56. package/dist/resolver.js.map +1 -1
  57. package/dist/value-promise.d.ts +1 -3
  58. package/package.json +9 -9
  59. package/src/binding-config.ts +1 -1
  60. package/src/binding-filter.ts +61 -9
  61. package/src/binding-inspector.ts +6 -8
  62. package/src/binding-sorter.ts +2 -2
  63. package/src/binding.ts +73 -9
  64. package/src/context-event.ts +30 -0
  65. package/src/context-observer.ts +1 -38
  66. package/src/context-subscription.ts +403 -0
  67. package/src/context-tag-indexer.ts +149 -0
  68. package/src/context-view.ts +3 -6
  69. package/src/context.ts +94 -293
  70. package/src/index.ts +5 -3
  71. package/src/inject-config.ts +3 -3
  72. package/src/inject.ts +19 -10
  73. package/src/interception-proxy.ts +25 -3
  74. package/src/interceptor-chain.ts +1 -1
  75. package/src/interceptor.ts +34 -8
  76. package/src/invocation.ts +26 -7
  77. package/src/keys.ts +7 -0
  78. package/src/resolution-session.ts +9 -5
  79. package/src/resolver.ts +5 -5
  80. package/src/value-promise.ts +1 -1
@@ -5,7 +5,8 @@
5
5
 
6
6
  import {Context} from './context';
7
7
  import {invokeMethodWithInterceptors} from './interceptor';
8
- import {InvocationArgs} from './invocation';
8
+ import {InvocationArgs, InvocationSource} from './invocation';
9
+ import {ResolutionSession} from './resolution-session';
9
10
  import {ValueOrPromise} from './value-promise';
10
11
 
11
12
  /**
@@ -57,13 +58,29 @@ export type AsInterceptedFunction<T> = T extends (
57
58
  */
58
59
  export type AsyncProxy<T> = {[P in keyof T]: AsInterceptedFunction<T[P]>};
59
60
 
61
+ /**
62
+ * Invocation source for injected proxies. It wraps a snapshot of the
63
+ * `ResolutionSession` that tracks the binding/injection stack.
64
+ */
65
+ export class ProxySource implements InvocationSource<ResolutionSession> {
66
+ type = 'proxy';
67
+ constructor(readonly value: ResolutionSession) {}
68
+
69
+ toString() {
70
+ return this.value.getBindingPath();
71
+ }
72
+ }
73
+
60
74
  /**
61
75
  * A proxy handler that applies interceptors
62
76
  *
63
77
  * See https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Proxy
64
78
  */
65
79
  export class InterceptionHandler<T extends object> implements ProxyHandler<T> {
66
- constructor(private context = new Context()) {}
80
+ constructor(
81
+ private context = new Context(),
82
+ private session?: ResolutionSession,
83
+ ) {}
67
84
 
68
85
  get(target: T, propertyName: PropertyKey, receiver: unknown) {
69
86
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -77,6 +94,7 @@ export class InterceptionHandler<T extends object> implements ProxyHandler<T> {
77
94
  target,
78
95
  propertyName,
79
96
  args,
97
+ {source: this.session && new ProxySource(this.session)},
80
98
  );
81
99
  };
82
100
  } else {
@@ -93,6 +111,10 @@ export class InterceptionHandler<T extends object> implements ProxyHandler<T> {
93
111
  export function createProxyWithInterceptors<T extends object>(
94
112
  target: T,
95
113
  context?: Context,
114
+ session?: ResolutionSession,
96
115
  ): AsyncProxy<T> {
97
- return new Proxy(target, new InterceptionHandler(context)) as AsyncProxy<T>;
116
+ return new Proxy(
117
+ target,
118
+ new InterceptionHandler(context, ResolutionSession.fork(session)),
119
+ ) as AsyncProxy<T>;
98
120
  }
@@ -3,7 +3,7 @@
3
3
  // This file is licensed under the MIT License.
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
- import * as debugFactory from 'debug';
6
+ import debugFactory from 'debug';
7
7
  import {BindingFilter} from './binding-filter';
8
8
  import {BindingAddress} from './binding-key';
9
9
  import {BindingComparator} from './binding-sorter';
@@ -11,11 +11,10 @@ import {
11
11
  MetadataMap,
12
12
  MethodDecoratorFactory,
13
13
  } from '@loopback/metadata';
14
- import * as assert from 'assert';
15
- import * as debugFactory from 'debug';
14
+ import assert from 'assert';
15
+ import debugFactory from 'debug';
16
16
  import {Binding, BindingTemplate} from './binding';
17
17
  import {bind} from './binding-decorator';
18
- import {filterByTag} from './binding-filter';
19
18
  import {BindingSpec} from './binding-inspector';
20
19
  import {sortBindingsByPhase} from './binding-sorter';
21
20
  import {Context} from './context';
@@ -47,15 +46,41 @@ export class InterceptedInvocationContext extends InvocationContext {
47
46
  * ContextTags.GLOBAL_INTERCEPTOR)
48
47
  */
49
48
  getGlobalInterceptorBindingKeys(): string[] {
50
- const bindings: Readonly<Binding<Interceptor>>[] = this.find(
51
- filterByTag(ContextTags.GLOBAL_INTERCEPTOR),
49
+ let bindings: Readonly<Binding<Interceptor>>[] = this.findByTag(
50
+ ContextTags.GLOBAL_INTERCEPTOR,
52
51
  );
52
+ bindings = bindings.filter(binding =>
53
+ // Only include interceptors that match the source type of the invocation
54
+ this.applicableTo(binding),
55
+ );
56
+
53
57
  this.sortGlobalInterceptorBindings(bindings);
54
58
  const keys = bindings.map(b => b.key);
55
59
  debug('Global interceptor binding keys:', keys);
56
60
  return keys;
57
61
  }
58
62
 
63
+ /**
64
+ * Check if the binding for a global interceptor matches the source type
65
+ * of the invocation
66
+ * @param binding - Binding
67
+ */
68
+ private applicableTo(binding: Readonly<Binding<unknown>>) {
69
+ const sourceType = this.source?.type;
70
+ // Unknown source type, always apply
71
+ if (sourceType == null) return true;
72
+ const allowedSource: string | string[] =
73
+ binding.tagMap[ContextTags.GLOBAL_INTERCEPTOR_SOURCE];
74
+ return (
75
+ // No tag, always apply
76
+ allowedSource == null ||
77
+ // source matched
78
+ allowedSource === sourceType ||
79
+ // source included in the string[]
80
+ (Array.isArray(allowedSource) && allowedSource.includes(sourceType))
81
+ );
82
+ }
83
+
59
84
  /**
60
85
  * Sort global interceptor bindings by `globalInterceptorGroup` tags
61
86
  * @param bindings - An array of global interceptor bindings
@@ -67,7 +92,7 @@ export class InterceptedInvocationContext extends InvocationContext {
67
92
  const orderedGroups =
68
93
  this.getSync(ContextBindings.GLOBAL_INTERCEPTOR_ORDERED_GROUPS, {
69
94
  optional: true,
70
- }) || [];
95
+ }) ?? [];
71
96
  return sortBindingsByPhase(
72
97
  bindings,
73
98
  ContextTags.GLOBAL_INTERCEPTOR_GROUP,
@@ -88,11 +113,11 @@ export class InterceptedInvocationContext extends InvocationContext {
88
113
  INTERCEPT_METHOD_KEY,
89
114
  this.target,
90
115
  this.methodName,
91
- ) || [];
116
+ ) ?? [];
92
117
  const targetClass =
93
118
  typeof this.target === 'function' ? this.target : this.target.constructor;
94
119
  const classInterceptors =
95
- MetadataInspector.getClassMetadata(INTERCEPT_CLASS_KEY, targetClass) ||
120
+ MetadataInspector.getClassMetadata(INTERCEPT_CLASS_KEY, targetClass) ??
96
121
  [];
97
122
  // Inserting class level interceptors before method level ones
98
123
  interceptors = mergeInterceptors(classInterceptors, interceptors);
@@ -305,6 +330,7 @@ export function invokeMethodWithInterceptors(
305
330
  target,
306
331
  methodName,
307
332
  args,
333
+ options.source,
308
334
  );
309
335
 
310
336
  invocationCtx.assertMethodExists();
package/src/invocation.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
6
  import {DecoratorFactory} from '@loopback/metadata';
7
- import * as assert from 'assert';
8
- import * as debugFactory from 'debug';
7
+ import assert from 'assert';
8
+ import debugFactory from 'debug';
9
9
  import {Context} from './context';
10
10
  import {invokeMethodWithInterceptors} from './interceptor';
11
11
  import {resolveInjectedArguments} from './resolver';
@@ -26,6 +26,20 @@ export type InvocationResult = any;
26
26
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
27
  export type InvocationArgs = any[];
28
28
 
29
+ /**
30
+ * An interface to represent the caller of the invocation
31
+ */
32
+ export interface InvocationSource<T = unknown> {
33
+ /**
34
+ * Type of the invoker, such as `proxy` and `route`
35
+ */
36
+ readonly type: string;
37
+ /**
38
+ * Metadata for the source, such as `ResolutionSession`
39
+ */
40
+ readonly value: T;
41
+ }
42
+
29
43
  /**
30
44
  * InvocationContext represents the context to invoke interceptors for a method.
31
45
  * The context can be used to access metadata about the invocation as well as
@@ -41,12 +55,11 @@ export class InvocationContext extends Context {
41
55
  * @param args - An array of arguments
42
56
  */
43
57
  constructor(
44
- // Make `parent` public so that the interceptor can add bindings to
45
- // the request context, for example, tracing id
46
- public readonly parent: Context,
58
+ parent: Context,
47
59
  public readonly target: object,
48
60
  public readonly methodName: string,
49
61
  public readonly args: InvocationArgs,
62
+ public readonly source?: InvocationSource,
50
63
  ) {
51
64
  super(parent);
52
65
  }
@@ -71,7 +84,8 @@ export class InvocationContext extends Context {
71
84
  * Description of the invocation
72
85
  */
73
86
  get description() {
74
- return `InvocationContext(${this.name}): ${this.targetName}`;
87
+ const source = this.source == null ? '' : `${this.source} => `;
88
+ return `InvocationContext(${this.name}): ${source}${this.targetName}`;
75
89
  }
76
90
 
77
91
  toString() {
@@ -129,6 +143,11 @@ export type InvocationOptions = {
129
143
  * Skip invocation of interceptors
130
144
  */
131
145
  skipInterceptors?: boolean;
146
+ /**
147
+ * Information about the source object that makes the invocation. For REST,
148
+ * it's a `Route`. For injected proxies, it's a `Binding`.
149
+ */
150
+ source?: InvocationSource;
132
151
  };
133
152
 
134
153
  /**
@@ -189,7 +208,7 @@ function invokeTargetMethodWithInjection(
189
208
  /* istanbul ignore if */
190
209
  if (debug.enabled) {
191
210
  debug('Invoking method %s', methodName);
192
- if (nonInjectedArgs && nonInjectedArgs.length) {
211
+ if (nonInjectedArgs?.length) {
193
212
  debug('Non-injected arguments:', nonInjectedArgs);
194
213
  }
195
214
  }
package/src/keys.ts CHANGED
@@ -40,6 +40,13 @@ export namespace ContextTags {
40
40
  */
41
41
  export const GLOBAL_INTERCEPTOR = 'globalInterceptor';
42
42
 
43
+ /**
44
+ * Binding tag for global interceptors to specify sources of invocations that
45
+ * the interceptor should apply. The tag value can be a string or string[], such
46
+ * as `'route'` or `['route', 'proxy']`.
47
+ */
48
+ export const GLOBAL_INTERCEPTOR_SOURCE = 'globalInterceptorSource';
49
+
43
50
  /**
44
51
  * Binding tag for group name of global interceptors
45
52
  */
@@ -4,7 +4,7 @@
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
6
  import {DecoratorFactory} from '@loopback/metadata';
7
- import * as debugModule from 'debug';
7
+ import debugModule from 'debug';
8
8
  import {Binding} from './binding';
9
9
  import {Injection} from './inject';
10
10
  import {BoundValue, tryWithFinally, ValueOrPromise} from './value-promise';
@@ -93,7 +93,7 @@ export class ResolutionSession {
93
93
  binding: Readonly<Binding>,
94
94
  session?: ResolutionSession,
95
95
  ): ResolutionSession {
96
- session = session || new ResolutionSession();
96
+ session = session ?? new ResolutionSession();
97
97
  session.pushBinding(binding);
98
98
  return session;
99
99
  }
@@ -125,7 +125,7 @@ export class ResolutionSession {
125
125
  injection: Readonly<Injection>,
126
126
  session?: ResolutionSession,
127
127
  ): ResolutionSession {
128
- session = session || new ResolutionSession();
128
+ session = session ?? new ResolutionSession();
129
129
  session.pushInjection(injection);
130
130
  return session;
131
131
  }
@@ -268,7 +268,7 @@ export class ResolutionSession {
268
268
  const binding = top.value;
269
269
  /* istanbul ignore if */
270
270
  if (debugSession.enabled) {
271
- debugSession('Exit binding:', binding && binding.toJSON());
271
+ debugSession('Exit binding:', binding?.toJSON());
272
272
  debugSession('Resolution path:', this.getResolutionPath() || '<empty>');
273
273
  }
274
274
  return binding;
@@ -321,6 +321,10 @@ export class ResolutionSession {
321
321
  getResolutionPath() {
322
322
  return this.stack.map(i => ResolutionSession.describe(i)).join(' --> ');
323
323
  }
324
+
325
+ toString() {
326
+ return this.getResolutionPath();
327
+ }
324
328
  }
325
329
 
326
330
  /**
@@ -363,5 +367,5 @@ export function asResolutionOptions(
363
367
  if (optionsOrSession instanceof ResolutionSession) {
364
368
  return {session: optionsOrSession};
365
369
  }
366
- return optionsOrSession || {};
370
+ return optionsOrSession ?? {};
367
371
  }
package/src/resolver.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  // License text available at https://opensource.org/licenses/MIT
5
5
 
6
6
  import {DecoratorFactory} from '@loopback/metadata';
7
- import * as assert from 'assert';
8
- import * as debugModule from 'debug';
7
+ import assert from 'assert';
8
+ import debugModule from 'debug';
9
9
  import {BindingScope} from './binding';
10
10
  import {isBindingAddress} from './binding-filter';
11
11
  import {BindingAddress} from './binding-key';
@@ -51,7 +51,7 @@ export function instantiateClass<T>(
51
51
  /* istanbul ignore if */
52
52
  if (debug.enabled) {
53
53
  debug('Instantiating %s', getTargetName(ctor));
54
- if (nonInjectedArgs && nonInjectedArgs.length) {
54
+ if (nonInjectedArgs?.length) {
55
55
  debug('Non-injected arguments:', nonInjectedArgs);
56
56
  }
57
57
  }
@@ -93,7 +93,7 @@ function resolveContext(
93
93
  injection: Readonly<Injection>,
94
94
  session?: ResolutionSession,
95
95
  ) {
96
- const currentBinding = session && session.currentBinding;
96
+ const currentBinding = session?.currentBinding;
97
97
  if (
98
98
  currentBinding == null ||
99
99
  currentBinding.scope !== BindingScope.SINGLETON
@@ -201,7 +201,7 @@ export function resolveInjectedArguments(
201
201
  // Example value:
202
202
  // [ , 'key1', , 'key2']
203
203
  const injectedArgs = describeInjectedArguments(target, method);
204
- const extraArgs = nonInjectedArgs || [];
204
+ const extraArgs = nonInjectedArgs ?? [];
205
205
 
206
206
  let argLength = DecoratorFactory.getNumberOfParameters(target, method);
207
207
 
@@ -29,7 +29,7 @@ export type BoundValue = any;
29
29
  */
30
30
  export type ValueOrPromise<T> = T | PromiseLike<T>;
31
31
 
32
- export type MapObject<T> = {[name: string]: T};
32
+ export type MapObject<T> = Record<string, T>;
33
33
 
34
34
  /**
35
35
  * Check whether a value is a Promise-like instance.