@loopback/context 4.0.0-alpha.8 → 4.1.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 (201) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +116 -0
  3. package/dist/binding-config.d.ts +40 -0
  4. package/dist/binding-config.js +33 -0
  5. package/dist/binding-config.js.map +1 -0
  6. package/dist/binding-decorator.d.ts +45 -0
  7. package/dist/binding-decorator.js +118 -0
  8. package/dist/binding-decorator.js.map +1 -0
  9. package/dist/binding-filter.d.ts +108 -0
  10. package/dist/binding-filter.js +162 -0
  11. package/dist/binding-filter.js.map +1 -0
  12. package/dist/binding-inspector.d.ts +150 -0
  13. package/dist/binding-inspector.js +249 -0
  14. package/dist/binding-inspector.js.map +1 -0
  15. package/dist/binding-key.d.ts +66 -0
  16. package/dist/binding-key.js +121 -0
  17. package/dist/binding-key.js.map +1 -0
  18. package/dist/binding-sorter.d.ts +71 -0
  19. package/dist/binding-sorter.js +89 -0
  20. package/dist/binding-sorter.js.map +1 -0
  21. package/dist/binding.d.ts +577 -0
  22. package/dist/binding.js +788 -0
  23. package/dist/binding.js.map +1 -0
  24. package/dist/context-event.d.ts +23 -0
  25. package/dist/context-event.js +7 -0
  26. package/dist/context-event.js.map +1 -0
  27. package/dist/context-observer.d.ts +36 -0
  28. package/dist/context-observer.js +7 -0
  29. package/dist/context-observer.js.map +1 -0
  30. package/dist/context-subscription.d.ts +147 -0
  31. package/dist/context-subscription.js +317 -0
  32. package/dist/context-subscription.js.map +1 -0
  33. package/dist/context-tag-indexer.d.ts +42 -0
  34. package/dist/context-tag-indexer.js +135 -0
  35. package/dist/context-tag-indexer.js.map +1 -0
  36. package/dist/context-view.d.ts +209 -0
  37. package/dist/context-view.js +240 -0
  38. package/dist/context-view.js.map +1 -0
  39. package/dist/context.d.ts +513 -0
  40. package/dist/context.js +717 -0
  41. package/dist/context.js.map +1 -0
  42. package/dist/index.d.ts +52 -0
  43. package/dist/index.js +60 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/inject-config.d.ts +67 -0
  46. package/dist/inject-config.js +181 -0
  47. package/dist/inject-config.js.map +1 -0
  48. package/dist/inject.d.ts +250 -0
  49. package/dist/inject.js +535 -0
  50. package/dist/inject.js.map +1 -0
  51. package/dist/interception-proxy.d.ts +76 -0
  52. package/dist/interception-proxy.js +67 -0
  53. package/dist/interception-proxy.js.map +1 -0
  54. package/dist/interceptor-chain.d.ts +121 -0
  55. package/dist/interceptor-chain.js +148 -0
  56. package/dist/interceptor-chain.js.map +1 -0
  57. package/dist/interceptor.d.ts +138 -0
  58. package/dist/interceptor.js +299 -0
  59. package/dist/interceptor.js.map +1 -0
  60. package/dist/invocation.d.ts +101 -0
  61. package/dist/invocation.js +163 -0
  62. package/dist/invocation.js.map +1 -0
  63. package/dist/json-types.d.ts +28 -0
  64. package/{lib/src/provider.js → dist/json-types.js} +3 -3
  65. package/dist/json-types.js.map +1 -0
  66. package/dist/keys.d.ts +65 -0
  67. package/dist/keys.js +74 -0
  68. package/dist/keys.js.map +1 -0
  69. package/dist/provider.d.ts +31 -0
  70. package/{lib6/src → dist}/provider.js +2 -2
  71. package/dist/provider.js.map +1 -0
  72. package/dist/resolution-session.d.ts +180 -0
  73. package/dist/resolution-session.js +274 -0
  74. package/dist/resolution-session.js.map +1 -0
  75. package/dist/resolver.d.ts +46 -0
  76. package/dist/resolver.js +203 -0
  77. package/dist/resolver.js.map +1 -0
  78. package/dist/unique-id.d.ts +14 -0
  79. package/dist/unique-id.js +26 -0
  80. package/dist/unique-id.js.map +1 -0
  81. package/dist/value-promise.d.ts +134 -0
  82. package/dist/value-promise.js +277 -0
  83. package/dist/value-promise.js.map +1 -0
  84. package/package.json +49 -34
  85. package/src/binding-config.ts +73 -0
  86. package/src/binding-decorator.ts +136 -0
  87. package/src/binding-filter.ts +250 -0
  88. package/src/binding-inspector.ts +371 -0
  89. package/src/binding-key.ts +136 -0
  90. package/src/binding-sorter.ts +124 -0
  91. package/src/binding.ts +1107 -0
  92. package/src/context-event.ts +30 -0
  93. package/src/context-observer.ts +50 -0
  94. package/src/context-subscription.ts +402 -0
  95. package/src/context-tag-indexer.ts +147 -0
  96. package/src/context-view.ts +440 -0
  97. package/src/context.ts +1079 -0
  98. package/src/index.ts +58 -0
  99. package/src/inject-config.ts +239 -0
  100. package/src/inject.ts +796 -0
  101. package/src/interception-proxy.ts +127 -0
  102. package/src/interceptor-chain.ts +268 -0
  103. package/src/interceptor.ts +430 -0
  104. package/src/invocation.ts +269 -0
  105. package/src/json-types.ts +35 -0
  106. package/src/keys.ts +85 -0
  107. package/src/provider.ts +37 -0
  108. package/src/resolution-session.ts +414 -0
  109. package/src/resolver.ts +282 -0
  110. package/src/unique-id.ts +24 -0
  111. package/src/value-promise.ts +318 -0
  112. package/index.d.ts +0 -6
  113. package/index.js +0 -9
  114. package/lib/binding.d.ts +0 -75
  115. package/lib/binding.js +0 -103
  116. package/lib/binding.js.map +0 -1
  117. package/lib/context.d.ts +0 -14
  118. package/lib/context.js +0 -97
  119. package/lib/context.js.map +0 -1
  120. package/lib/index.d.ts +0 -1
  121. package/lib/index.js +0 -12
  122. package/lib/index.js.map +0 -1
  123. package/lib/inject.d.ts +0 -46
  124. package/lib/inject.js +0 -74
  125. package/lib/inject.js.map +0 -1
  126. package/lib/isPromise.d.ts +0 -1
  127. package/lib/isPromise.js +0 -15
  128. package/lib/isPromise.js.map +0 -1
  129. package/lib/reflect.d.ts +0 -39
  130. package/lib/reflect.js +0 -20
  131. package/lib/reflect.js.map +0 -1
  132. package/lib/resolver.d.ts +0 -30
  133. package/lib/resolver.js +0 -129
  134. package/lib/resolver.js.map +0 -1
  135. package/lib/src/binding.d.ts +0 -85
  136. package/lib/src/binding.js +0 -123
  137. package/lib/src/binding.js.map +0 -1
  138. package/lib/src/context.d.ts +0 -14
  139. package/lib/src/context.js +0 -97
  140. package/lib/src/context.js.map +0 -1
  141. package/lib/src/index.d.ts +0 -10
  142. package/lib/src/index.js +0 -27
  143. package/lib/src/index.js.map +0 -1
  144. package/lib/src/inject.d.ts +0 -46
  145. package/lib/src/inject.js +0 -74
  146. package/lib/src/inject.js.map +0 -1
  147. package/lib/src/isPromise.d.ts +0 -1
  148. package/lib/src/isPromise.js +0 -15
  149. package/lib/src/isPromise.js.map +0 -1
  150. package/lib/src/provider.d.ts +0 -29
  151. package/lib/src/provider.js.map +0 -1
  152. package/lib/src/reflect.d.ts +0 -38
  153. package/lib/src/reflect.js +0 -143
  154. package/lib/src/reflect.js.map +0 -1
  155. package/lib/src/resolver.d.ts +0 -34
  156. package/lib/src/resolver.js +0 -144
  157. package/lib/src/resolver.js.map +0 -1
  158. package/lib6/binding.d.ts +0 -75
  159. package/lib6/binding.js +0 -103
  160. package/lib6/binding.js.map +0 -1
  161. package/lib6/context.d.ts +0 -14
  162. package/lib6/context.js +0 -97
  163. package/lib6/context.js.map +0 -1
  164. package/lib6/index.d.ts +0 -1
  165. package/lib6/index.js +0 -12
  166. package/lib6/index.js.map +0 -1
  167. package/lib6/inject.d.ts +0 -46
  168. package/lib6/inject.js +0 -74
  169. package/lib6/inject.js.map +0 -1
  170. package/lib6/isPromise.d.ts +0 -1
  171. package/lib6/isPromise.js +0 -15
  172. package/lib6/isPromise.js.map +0 -1
  173. package/lib6/reflect.d.ts +0 -39
  174. package/lib6/reflect.js +0 -20
  175. package/lib6/reflect.js.map +0 -1
  176. package/lib6/resolver.d.ts +0 -30
  177. package/lib6/resolver.js +0 -129
  178. package/lib6/resolver.js.map +0 -1
  179. package/lib6/src/binding.d.ts +0 -85
  180. package/lib6/src/binding.js +0 -133
  181. package/lib6/src/binding.js.map +0 -1
  182. package/lib6/src/context.d.ts +0 -14
  183. package/lib6/src/context.js +0 -97
  184. package/lib6/src/context.js.map +0 -1
  185. package/lib6/src/index.d.ts +0 -10
  186. package/lib6/src/index.js +0 -27
  187. package/lib6/src/index.js.map +0 -1
  188. package/lib6/src/inject.d.ts +0 -46
  189. package/lib6/src/inject.js +0 -74
  190. package/lib6/src/inject.js.map +0 -1
  191. package/lib6/src/isPromise.d.ts +0 -1
  192. package/lib6/src/isPromise.js +0 -15
  193. package/lib6/src/isPromise.js.map +0 -1
  194. package/lib6/src/provider.d.ts +0 -29
  195. package/lib6/src/provider.js.map +0 -1
  196. package/lib6/src/reflect.d.ts +0 -38
  197. package/lib6/src/reflect.js +0 -143
  198. package/lib6/src/reflect.js.map +0 -1
  199. package/lib6/src/resolver.d.ts +0 -34
  200. package/lib6/src/resolver.js +0 -154
  201. package/lib6/src/resolver.js.map +0 -1
@@ -0,0 +1,430 @@
1
+ // Copyright IBM Corp. 2019,2020. All Rights Reserved.
2
+ // Node module: @loopback/context
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {
7
+ ClassDecoratorFactory,
8
+ DecoratorFactory,
9
+ MetadataAccessor,
10
+ MetadataInspector,
11
+ MetadataMap,
12
+ MethodDecoratorFactory,
13
+ } from '@loopback/metadata';
14
+ import assert from 'assert';
15
+ import debugFactory from 'debug';
16
+ import {Binding, BindingTemplate} from './binding';
17
+ import {injectable} from './binding-decorator';
18
+ import {
19
+ BindingFromClassOptions,
20
+ BindingSpec,
21
+ createBindingFromClass,
22
+ isProviderClass,
23
+ } from './binding-inspector';
24
+ import {BindingAddress, BindingKey} from './binding-key';
25
+ import {sortBindingsByPhase} from './binding-sorter';
26
+ import {Context} from './context';
27
+ import {
28
+ GenericInterceptor,
29
+ GenericInterceptorOrKey,
30
+ invokeInterceptors,
31
+ } from './interceptor-chain';
32
+ import {
33
+ InvocationArgs,
34
+ InvocationContext,
35
+ InvocationOptions,
36
+ InvocationResult,
37
+ } from './invocation';
38
+ import {
39
+ ContextBindings,
40
+ ContextTags,
41
+ GLOBAL_INTERCEPTOR_NAMESPACE,
42
+ LOCAL_INTERCEPTOR_NAMESPACE,
43
+ } from './keys';
44
+ import {Provider} from './provider';
45
+ import {Constructor, tryWithFinally, ValueOrPromise} from './value-promise';
46
+ const debug = debugFactory('loopback:context:interceptor');
47
+
48
+ /**
49
+ * A specialized InvocationContext for interceptors
50
+ */
51
+ export class InterceptedInvocationContext extends InvocationContext {
52
+ /**
53
+ * Discover all binding keys for global interceptors (tagged by
54
+ * ContextTags.GLOBAL_INTERCEPTOR)
55
+ */
56
+ getGlobalInterceptorBindingKeys(): string[] {
57
+ let bindings: Readonly<Binding<Interceptor>>[] = this.findByTag(
58
+ ContextTags.GLOBAL_INTERCEPTOR,
59
+ );
60
+ bindings = bindings.filter(binding =>
61
+ // Only include interceptors that match the source type of the invocation
62
+ this.applicableTo(binding),
63
+ );
64
+
65
+ this.sortGlobalInterceptorBindings(bindings);
66
+ const keys = bindings.map(b => b.key);
67
+ debug('Global interceptor binding keys:', keys);
68
+ return keys;
69
+ }
70
+
71
+ /**
72
+ * Check if the binding for a global interceptor matches the source type
73
+ * of the invocation
74
+ * @param binding - Binding
75
+ */
76
+ private applicableTo(binding: Readonly<Binding<unknown>>) {
77
+ const sourceType = this.source?.type;
78
+ // Unknown source type, always apply
79
+ if (sourceType == null) return true;
80
+ const allowedSource: string | string[] =
81
+ binding.tagMap[ContextTags.GLOBAL_INTERCEPTOR_SOURCE];
82
+ return (
83
+ // No tag, always apply
84
+ allowedSource == null ||
85
+ // source matched
86
+ allowedSource === sourceType ||
87
+ // source included in the string[]
88
+ (Array.isArray(allowedSource) && allowedSource.includes(sourceType))
89
+ );
90
+ }
91
+
92
+ /**
93
+ * Sort global interceptor bindings by `globalInterceptorGroup` tags
94
+ * @param bindings - An array of global interceptor bindings
95
+ */
96
+ private sortGlobalInterceptorBindings(
97
+ bindings: Readonly<Binding<Interceptor>>[],
98
+ ) {
99
+ // Get predefined ordered groups for global interceptors
100
+ const orderedGroups =
101
+ this.getSync(ContextBindings.GLOBAL_INTERCEPTOR_ORDERED_GROUPS, {
102
+ optional: true,
103
+ }) ?? [];
104
+ return sortBindingsByPhase(
105
+ bindings,
106
+ ContextTags.GLOBAL_INTERCEPTOR_GROUP,
107
+ orderedGroups,
108
+ );
109
+ }
110
+
111
+ /**
112
+ * Load all interceptors for the given invocation context. It adds
113
+ * interceptors from possibly three sources:
114
+ * 1. method level `@intercept`
115
+ * 2. class level `@intercept`
116
+ * 3. global interceptors discovered in the context
117
+ */
118
+ loadInterceptors() {
119
+ let interceptors =
120
+ MetadataInspector.getMethodMetadata(
121
+ INTERCEPT_METHOD_KEY,
122
+ this.target,
123
+ this.methodName,
124
+ ) ?? [];
125
+ const targetClass =
126
+ typeof this.target === 'function' ? this.target : this.target.constructor;
127
+ const classInterceptors =
128
+ MetadataInspector.getClassMetadata(INTERCEPT_CLASS_KEY, targetClass) ??
129
+ [];
130
+ // Inserting class level interceptors before method level ones
131
+ interceptors = mergeInterceptors(classInterceptors, interceptors);
132
+ const globalInterceptors = this.getGlobalInterceptorBindingKeys();
133
+ // Inserting global interceptors
134
+ interceptors = mergeInterceptors(globalInterceptors, interceptors);
135
+ debug('Interceptors for %s', this.targetName, interceptors);
136
+ return interceptors;
137
+ }
138
+ }
139
+
140
+ /**
141
+ * The `BindingTemplate` function to configure a binding as a global interceptor
142
+ * by tagging it with `ContextTags.INTERCEPTOR`
143
+ * @param group - Group for ordering the interceptor
144
+ */
145
+ export function asGlobalInterceptor(group?: string): BindingTemplate {
146
+ return binding => {
147
+ binding
148
+ // Tagging with `GLOBAL_INTERCEPTOR` is required.
149
+ .tag(ContextTags.GLOBAL_INTERCEPTOR)
150
+ // `GLOBAL_INTERCEPTOR_NAMESPACE` is to make the binding key more readable.
151
+ .tag({[ContextTags.NAMESPACE]: GLOBAL_INTERCEPTOR_NAMESPACE});
152
+ if (group) binding.tag({[ContextTags.GLOBAL_INTERCEPTOR_GROUP]: group});
153
+ };
154
+ }
155
+
156
+ /**
157
+ * `@globalInterceptor` decorator to mark the class as a global interceptor
158
+ * @param group - Group for ordering the interceptor
159
+ * @param specs - Extra binding specs
160
+ */
161
+ export function globalInterceptor(group?: string, ...specs: BindingSpec[]) {
162
+ return injectable(asGlobalInterceptor(group), ...specs);
163
+ }
164
+
165
+ /**
166
+ * Interceptor function to intercept method invocations
167
+ */
168
+ export interface Interceptor extends GenericInterceptor<InvocationContext> {}
169
+
170
+ /**
171
+ * Interceptor function or binding key that can be used as parameters for
172
+ * `@intercept()`
173
+ */
174
+ export type InterceptorOrKey = GenericInterceptorOrKey<InvocationContext>;
175
+
176
+ /**
177
+ * Metadata key for method-level interceptors
178
+ */
179
+ export const INTERCEPT_METHOD_KEY = MetadataAccessor.create<
180
+ InterceptorOrKey[],
181
+ MethodDecorator
182
+ >('intercept:method');
183
+
184
+ /**
185
+ * Adding interceptors from the spec to the front of existing ones. Duplicate
186
+ * entries are eliminated from the spec side.
187
+ *
188
+ * For example:
189
+ *
190
+ * - [log] + [cache, log] => [cache, log]
191
+ * - [log] + [log, cache] => [log, cache]
192
+ * - [] + [cache, log] => [cache, log]
193
+ * - [cache, log] + [] => [cache, log]
194
+ * - [log] + [cache] => [log, cache]
195
+ *
196
+ * @param interceptorsFromSpec - Interceptors from `@intercept`
197
+ * @param existingInterceptors - Interceptors already applied for the method
198
+ */
199
+ export function mergeInterceptors(
200
+ interceptorsFromSpec: InterceptorOrKey[],
201
+ existingInterceptors: InterceptorOrKey[],
202
+ ) {
203
+ const interceptorsToApply = new Set(interceptorsFromSpec);
204
+ const appliedInterceptors = new Set(existingInterceptors);
205
+ // Remove interceptors that already exist
206
+ for (const i of interceptorsToApply) {
207
+ if (appliedInterceptors.has(i)) {
208
+ interceptorsToApply.delete(i);
209
+ }
210
+ }
211
+ // Add existing interceptors after ones from the spec
212
+ for (const i of appliedInterceptors) {
213
+ interceptorsToApply.add(i);
214
+ }
215
+ return Array.from(interceptorsToApply);
216
+ }
217
+
218
+ /**
219
+ * Metadata key for method-level interceptors
220
+ */
221
+ export const INTERCEPT_CLASS_KEY = MetadataAccessor.create<
222
+ InterceptorOrKey[],
223
+ ClassDecorator
224
+ >('intercept:class');
225
+
226
+ /**
227
+ * A factory to define `@intercept` for classes. It allows `@intercept` to be
228
+ * used multiple times on the same class.
229
+ */
230
+ class InterceptClassDecoratorFactory extends ClassDecoratorFactory<
231
+ InterceptorOrKey[]
232
+ > {
233
+ protected mergeWithOwn(ownMetadata: InterceptorOrKey[], target: Object) {
234
+ ownMetadata = ownMetadata || [];
235
+ return mergeInterceptors(this.spec, ownMetadata);
236
+ }
237
+ }
238
+
239
+ /**
240
+ * A factory to define `@intercept` for methods. It allows `@intercept` to be
241
+ * used multiple times on the same method.
242
+ */
243
+ class InterceptMethodDecoratorFactory extends MethodDecoratorFactory<
244
+ InterceptorOrKey[]
245
+ > {
246
+ protected mergeWithOwn(
247
+ ownMetadata: MetadataMap<InterceptorOrKey[]>,
248
+ target: Object,
249
+ methodName: string,
250
+ methodDescriptor: TypedPropertyDescriptor<unknown>,
251
+ ) {
252
+ ownMetadata = ownMetadata || {};
253
+ const interceptors = ownMetadata[methodName] || [];
254
+
255
+ // Adding interceptors to the list
256
+ ownMetadata[methodName] = mergeInterceptors(this.spec, interceptors);
257
+
258
+ return ownMetadata;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Decorator function `@intercept` for classes/methods to apply interceptors. It
264
+ * can be applied on a class and its public methods. Multiple occurrences of
265
+ * `@intercept` are allowed on the same target class or method. The decorator
266
+ * takes a list of `interceptor` functions or binding keys.
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * @intercept(log, metrics)
271
+ * class MyController {
272
+ * @intercept('caching-interceptor')
273
+ * @intercept('name-validation-interceptor')
274
+ * greet(name: string) {
275
+ * return `Hello, ${name}`;
276
+ * }
277
+ * }
278
+ * ```
279
+ *
280
+ * @param interceptorOrKeys - One or more interceptors or binding keys that are
281
+ * resolved to be interceptors
282
+ */
283
+ export function intercept(...interceptorOrKeys: InterceptorOrKey[]) {
284
+ return function interceptDecoratorForClassOrMethod(
285
+ // Class or a prototype
286
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
287
+ target: any,
288
+ method?: string,
289
+ // Use `any` to for `TypedPropertyDescriptor`
290
+ // See https://github.com/loopbackio/loopback-next/pull/2704
291
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
292
+ methodDescriptor?: TypedPropertyDescriptor<any>,
293
+ ) {
294
+ if (method && methodDescriptor) {
295
+ // Method
296
+ return InterceptMethodDecoratorFactory.createDecorator(
297
+ INTERCEPT_METHOD_KEY,
298
+ interceptorOrKeys,
299
+ {decoratorName: '@intercept'},
300
+ )(target, method, methodDescriptor!);
301
+ }
302
+ if (typeof target === 'function' && !method && !methodDescriptor) {
303
+ // Class
304
+ return InterceptClassDecoratorFactory.createDecorator(
305
+ INTERCEPT_CLASS_KEY,
306
+ interceptorOrKeys,
307
+ {decoratorName: '@intercept'},
308
+ )(target);
309
+ }
310
+ // Not on a class or method
311
+ throw new Error(
312
+ '@intercept cannot be used on a property: ' +
313
+ DecoratorFactory.getTargetName(target, method, methodDescriptor),
314
+ );
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Invoke a method with the given context
320
+ * @param context - Context object
321
+ * @param target - Target class (for static methods) or object (for instance methods)
322
+ * @param methodName - Method name
323
+ * @param args - An array of argument values
324
+ * @param options - Options for the invocation
325
+ */
326
+ export function invokeMethodWithInterceptors(
327
+ context: Context,
328
+ target: object,
329
+ methodName: string,
330
+ args: InvocationArgs,
331
+ options: InvocationOptions = {},
332
+ ): ValueOrPromise<InvocationResult> {
333
+ // Do not allow `skipInterceptors` as it's against the function name
334
+ // `invokeMethodWithInterceptors`
335
+ assert(!options.skipInterceptors, 'skipInterceptors is not allowed');
336
+ const invocationCtx = new InterceptedInvocationContext(
337
+ context,
338
+ target,
339
+ methodName,
340
+ args,
341
+ options.source,
342
+ );
343
+
344
+ invocationCtx.assertMethodExists();
345
+ return tryWithFinally(
346
+ () => {
347
+ const interceptors = invocationCtx.loadInterceptors();
348
+ const targetMethodInvoker = () =>
349
+ invocationCtx.invokeTargetMethod(options);
350
+ interceptors.push(targetMethodInvoker);
351
+ return invokeInterceptors(invocationCtx, interceptors);
352
+ },
353
+ () => invocationCtx.close(),
354
+ );
355
+ }
356
+
357
+ /**
358
+ * Options for an interceptor binding
359
+ */
360
+ export interface InterceptorBindingOptions extends BindingFromClassOptions {
361
+ /**
362
+ * Global or local interceptor
363
+ */
364
+ global?: boolean;
365
+ /**
366
+ * Group name for a global interceptor
367
+ */
368
+ group?: string;
369
+ /**
370
+ * Source filter for a global interceptor
371
+ */
372
+ source?: string | string[];
373
+ }
374
+
375
+ /**
376
+ * Register an interceptor function or provider class to the given context
377
+ * @param ctx - Context object
378
+ * @param interceptor - An interceptor function or provider class
379
+ * @param options - Options for the interceptor binding
380
+ */
381
+ export function registerInterceptor(
382
+ ctx: Context,
383
+ interceptor: Interceptor | Constructor<Provider<Interceptor>>,
384
+ options: InterceptorBindingOptions = {},
385
+ ) {
386
+ let {global} = options;
387
+ const {group, source} = options;
388
+ if (group != null || source != null) {
389
+ // If group or source is set, assuming global
390
+ global = global !== false;
391
+ }
392
+
393
+ const namespace =
394
+ options.namespace ?? options.defaultNamespace ?? global
395
+ ? GLOBAL_INTERCEPTOR_NAMESPACE
396
+ : LOCAL_INTERCEPTOR_NAMESPACE;
397
+
398
+ let binding: Binding<Interceptor>;
399
+ if (isProviderClass(interceptor)) {
400
+ binding = createBindingFromClass(interceptor, {
401
+ defaultNamespace: namespace,
402
+ ...options,
403
+ });
404
+ if (binding.tagMap[ContextTags.GLOBAL_INTERCEPTOR]) {
405
+ global = true;
406
+ }
407
+ ctx.add(binding);
408
+ } else {
409
+ let key = options.key;
410
+ if (!key) {
411
+ const name = options.name ?? interceptor.name;
412
+ if (!name) {
413
+ key = BindingKey.generate<Interceptor>(namespace).key;
414
+ } else {
415
+ key = `${namespace}.${name}`;
416
+ }
417
+ }
418
+ binding = ctx
419
+ .bind(key as BindingAddress<Interceptor>)
420
+ .to(interceptor as Interceptor);
421
+ }
422
+ if (global) {
423
+ binding.apply(asGlobalInterceptor(group));
424
+ if (source) {
425
+ binding.tag({[ContextTags.GLOBAL_INTERCEPTOR_SOURCE]: source});
426
+ }
427
+ }
428
+
429
+ return binding;
430
+ }
@@ -0,0 +1,269 @@
1
+ // Copyright IBM Corp. 2019,2020. All Rights Reserved.
2
+ // Node module: @loopback/context
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {DecoratorFactory} from '@loopback/metadata';
7
+ import assert from 'assert';
8
+ import debugFactory from 'debug';
9
+ import {Context} from './context';
10
+ import {invokeMethodWithInterceptors} from './interceptor';
11
+ import {ResolutionSession} from './resolution-session';
12
+ import {resolveInjectedArguments} from './resolver';
13
+ import {transformValueOrPromise, ValueOrPromise} from './value-promise';
14
+
15
+ const debug = debugFactory('loopback:context:invocation');
16
+ const getTargetName = DecoratorFactory.getTargetName;
17
+
18
+ /**
19
+ * Return value for a method invocation
20
+ */
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ export type InvocationResult = any;
23
+
24
+ /**
25
+ * Array of arguments for a method invocation
26
+ */
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ export type InvocationArgs = any[];
29
+
30
+ /**
31
+ * An interface to represent the caller of the invocation
32
+ */
33
+ export interface InvocationSource<T = unknown> {
34
+ /**
35
+ * Type of the invoker, such as `proxy` and `route`
36
+ */
37
+ readonly type: string;
38
+ /**
39
+ * Metadata for the source, such as `ResolutionSession`
40
+ */
41
+ readonly value: T;
42
+ }
43
+
44
+ /**
45
+ * InvocationContext represents the context to invoke interceptors for a method.
46
+ * The context can be used to access metadata about the invocation as well as
47
+ * other dependencies.
48
+ */
49
+ export class InvocationContext extends Context {
50
+ /**
51
+ * Construct a new instance of `InvocationContext`
52
+ * @param parent - Parent context, such as the RequestContext
53
+ * @param target - Target class (for static methods) or prototype/object
54
+ * (for instance methods)
55
+ * @param methodName - Method name
56
+ * @param args - An array of arguments
57
+ */
58
+ constructor(
59
+ parent: Context,
60
+ public readonly target: object,
61
+ public readonly methodName: string,
62
+ public readonly args: InvocationArgs,
63
+ public readonly source?: InvocationSource,
64
+ ) {
65
+ super(parent);
66
+ }
67
+
68
+ /**
69
+ * The target class, such as `OrderController`
70
+ */
71
+ get targetClass() {
72
+ return typeof this.target === 'function'
73
+ ? this.target
74
+ : this.target.constructor;
75
+ }
76
+
77
+ /**
78
+ * The target name, such as `OrderController.prototype.cancelOrder`
79
+ */
80
+ get targetName() {
81
+ return getTargetName(this.target, this.methodName);
82
+ }
83
+
84
+ /**
85
+ * Description of the invocation
86
+ */
87
+ get description() {
88
+ const source = this.source == null ? '' : `${this.source} => `;
89
+ return `InvocationContext(${this.name}): ${source}${this.targetName}`;
90
+ }
91
+
92
+ toString() {
93
+ return this.description;
94
+ }
95
+
96
+ /**
97
+ * Assert the method exists on the target. An error will be thrown if otherwise.
98
+ * @param context - Invocation context
99
+ */
100
+ assertMethodExists() {
101
+ const targetWithMethods = this.target as Record<string, Function>;
102
+ if (typeof targetWithMethods[this.methodName] !== 'function') {
103
+ const targetName = getTargetName(this.target, this.methodName);
104
+ assert(false, `Method ${targetName} not found`);
105
+ }
106
+ return targetWithMethods;
107
+ }
108
+
109
+ /**
110
+ * Invoke the target method with the given context
111
+ * @param context - Invocation context
112
+ * @param options - Options for the invocation
113
+ */
114
+ invokeTargetMethod(
115
+ options: InvocationOptions = {skipParameterInjection: true},
116
+ ) {
117
+ const targetWithMethods = this.assertMethodExists();
118
+ if (!options.skipParameterInjection) {
119
+ return invokeTargetMethodWithInjection(
120
+ this,
121
+ targetWithMethods,
122
+ this.methodName,
123
+ this.args,
124
+ options.session,
125
+ );
126
+ }
127
+ return invokeTargetMethod(
128
+ this,
129
+ targetWithMethods,
130
+ this.methodName,
131
+ this.args,
132
+ );
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Options to control invocations
138
+ */
139
+ export type InvocationOptions = {
140
+ /**
141
+ * Skip dependency injection on method parameters
142
+ */
143
+ skipParameterInjection?: boolean;
144
+ /**
145
+ * Skip invocation of interceptors
146
+ */
147
+ skipInterceptors?: boolean;
148
+ /**
149
+ * Information about the source object that makes the invocation. For REST,
150
+ * it's a `Route`. For injected proxies, it's a `Binding`.
151
+ */
152
+ source?: InvocationSource;
153
+ /**
154
+ * Resolution session
155
+ */
156
+ session?: ResolutionSession;
157
+ };
158
+
159
+ /**
160
+ * Invoke a method using dependency injection. Interceptors are invoked as part
161
+ * of the invocation.
162
+ * @param target - Target of the method, it will be the class for a static
163
+ * method, and instance or class prototype for a prototype method
164
+ * @param method - Name of the method
165
+ * @param ctx - Context object
166
+ * @param nonInjectedArgs - Optional array of args for non-injected parameters
167
+ * @param options - Options for the invocation
168
+ */
169
+ export function invokeMethod(
170
+ target: object,
171
+ method: string,
172
+ ctx: Context,
173
+ nonInjectedArgs: InvocationArgs = [],
174
+ options: InvocationOptions = {},
175
+ ): ValueOrPromise<InvocationResult> {
176
+ if (options.skipInterceptors) {
177
+ if (options.skipParameterInjection) {
178
+ // Invoke the target method directly without injection or interception
179
+ return invokeTargetMethod(ctx, target, method, nonInjectedArgs);
180
+ } else {
181
+ return invokeTargetMethodWithInjection(
182
+ ctx,
183
+ target,
184
+ method,
185
+ nonInjectedArgs,
186
+ options.session,
187
+ );
188
+ }
189
+ }
190
+ // Invoke the target method with interception but no injection
191
+ return invokeMethodWithInterceptors(
192
+ ctx,
193
+ target,
194
+ method,
195
+ nonInjectedArgs,
196
+ options,
197
+ );
198
+ }
199
+
200
+ /**
201
+ * Invoke a method. Method parameter dependency injection is honored.
202
+ * @param target - Target of the method, it will be the class for a static
203
+ * method, and instance or class prototype for a prototype method
204
+ * @param method - Name of the method
205
+ * @param ctx - Context
206
+ * @param nonInjectedArgs - Optional array of args for non-injected parameters
207
+ */
208
+ function invokeTargetMethodWithInjection(
209
+ ctx: Context,
210
+ target: object,
211
+ method: string,
212
+ nonInjectedArgs?: InvocationArgs,
213
+ session?: ResolutionSession,
214
+ ): ValueOrPromise<InvocationResult> {
215
+ const methodName = getTargetName(target, method);
216
+ /* istanbul ignore if */
217
+ if (debug.enabled) {
218
+ debug('Invoking method %s', methodName);
219
+ if (nonInjectedArgs?.length) {
220
+ debug('Non-injected arguments:', nonInjectedArgs);
221
+ }
222
+ }
223
+ const argsOrPromise = resolveInjectedArguments(
224
+ target,
225
+ method,
226
+ ctx,
227
+ session,
228
+ nonInjectedArgs,
229
+ );
230
+ const targetWithMethods = target as Record<string, Function>;
231
+ assert(
232
+ typeof targetWithMethods[method] === 'function',
233
+ `Method ${method} not found`,
234
+ );
235
+ return transformValueOrPromise(argsOrPromise, args => {
236
+ /* istanbul ignore if */
237
+ if (debug.enabled) {
238
+ debug('Injected arguments for %s:', methodName, args);
239
+ }
240
+ return invokeTargetMethod(ctx, targetWithMethods, method, args);
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Invoke the target method
246
+ * @param ctx - Context object
247
+ * @param target - Target class or object
248
+ * @param methodName - Target method name
249
+ * @param args - Arguments
250
+ */
251
+ function invokeTargetMethod(
252
+ ctx: Context, // Not used
253
+ target: object,
254
+ methodName: string,
255
+ args: InvocationArgs,
256
+ ): InvocationResult {
257
+ const targetWithMethods = target as Record<string, Function>;
258
+ /* istanbul ignore if */
259
+ if (debug.enabled) {
260
+ debug('Invoking method %s', getTargetName(target, methodName), args);
261
+ }
262
+ // Invoke the target method
263
+ const result = targetWithMethods[methodName](...args);
264
+ /* istanbul ignore if */
265
+ if (debug.enabled) {
266
+ debug('Method invoked: %s', getTargetName(target, methodName), result);
267
+ }
268
+ return result;
269
+ }