@loopback/context 4.0.0-alpha.9 → 4.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.
- package/LICENSE +25 -0
- package/README.md +116 -0
- package/dist/binding-config.d.ts +40 -0
- package/dist/binding-config.js +33 -0
- package/dist/binding-config.js.map +1 -0
- package/dist/binding-decorator.d.ts +45 -0
- package/dist/binding-decorator.js +118 -0
- package/dist/binding-decorator.js.map +1 -0
- package/dist/binding-filter.d.ts +108 -0
- package/dist/binding-filter.js +162 -0
- package/dist/binding-filter.js.map +1 -0
- package/dist/binding-inspector.d.ts +150 -0
- package/dist/binding-inspector.js +249 -0
- package/dist/binding-inspector.js.map +1 -0
- package/dist/binding-key.d.ts +66 -0
- package/dist/binding-key.js +121 -0
- package/dist/binding-key.js.map +1 -0
- package/dist/binding-sorter.d.ts +71 -0
- package/dist/binding-sorter.js +89 -0
- package/dist/binding-sorter.js.map +1 -0
- package/dist/binding.d.ts +577 -0
- package/dist/binding.js +788 -0
- package/dist/binding.js.map +1 -0
- package/dist/context-event.d.ts +23 -0
- package/dist/context-event.js +7 -0
- package/dist/context-event.js.map +1 -0
- package/dist/context-observer.d.ts +36 -0
- package/dist/context-observer.js +7 -0
- package/dist/context-observer.js.map +1 -0
- package/dist/context-subscription.d.ts +147 -0
- package/dist/context-subscription.js +317 -0
- package/dist/context-subscription.js.map +1 -0
- package/dist/context-tag-indexer.d.ts +42 -0
- package/dist/context-tag-indexer.js +135 -0
- package/dist/context-tag-indexer.js.map +1 -0
- package/dist/context-view.d.ts +209 -0
- package/dist/context-view.js +240 -0
- package/dist/context-view.js.map +1 -0
- package/dist/context.d.ts +513 -0
- package/dist/context.js +717 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -0
- package/dist/inject-config.d.ts +67 -0
- package/dist/inject-config.js +181 -0
- package/dist/inject-config.js.map +1 -0
- package/dist/inject.d.ts +250 -0
- package/dist/inject.js +535 -0
- package/dist/inject.js.map +1 -0
- package/dist/interception-proxy.d.ts +76 -0
- package/dist/interception-proxy.js +67 -0
- package/dist/interception-proxy.js.map +1 -0
- package/dist/interceptor-chain.d.ts +121 -0
- package/dist/interceptor-chain.js +148 -0
- package/dist/interceptor-chain.js.map +1 -0
- package/dist/interceptor.d.ts +138 -0
- package/dist/interceptor.js +299 -0
- package/dist/interceptor.js.map +1 -0
- package/dist/invocation.d.ts +101 -0
- package/dist/invocation.js +163 -0
- package/dist/invocation.js.map +1 -0
- package/dist/json-types.d.ts +28 -0
- package/{lib/src/provider.js → dist/json-types.js} +3 -3
- package/dist/json-types.js.map +1 -0
- package/dist/keys.d.ts +65 -0
- package/dist/keys.js +74 -0
- package/dist/keys.js.map +1 -0
- package/dist/provider.d.ts +31 -0
- package/{lib6/src → dist}/provider.js +2 -2
- package/dist/provider.js.map +1 -0
- package/dist/resolution-session.d.ts +180 -0
- package/dist/resolution-session.js +274 -0
- package/dist/resolution-session.js.map +1 -0
- package/dist/resolver.d.ts +46 -0
- package/dist/resolver.js +203 -0
- package/dist/resolver.js.map +1 -0
- package/dist/unique-id.d.ts +14 -0
- package/dist/unique-id.js +26 -0
- package/dist/unique-id.js.map +1 -0
- package/dist/value-promise.d.ts +134 -0
- package/dist/value-promise.js +277 -0
- package/dist/value-promise.js.map +1 -0
- package/package.json +49 -34
- package/src/binding-config.ts +73 -0
- package/src/binding-decorator.ts +136 -0
- package/src/binding-filter.ts +250 -0
- package/src/binding-inspector.ts +371 -0
- package/src/binding-key.ts +136 -0
- package/src/binding-sorter.ts +124 -0
- package/src/binding.ts +1107 -0
- package/src/context-event.ts +30 -0
- package/src/context-observer.ts +50 -0
- package/src/context-subscription.ts +402 -0
- package/src/context-tag-indexer.ts +147 -0
- package/src/context-view.ts +440 -0
- package/src/context.ts +1079 -0
- package/src/index.ts +58 -0
- package/src/inject-config.ts +239 -0
- package/src/inject.ts +796 -0
- package/src/interception-proxy.ts +127 -0
- package/src/interceptor-chain.ts +268 -0
- package/src/interceptor.ts +430 -0
- package/src/invocation.ts +269 -0
- package/src/json-types.ts +35 -0
- package/src/keys.ts +85 -0
- package/src/provider.ts +37 -0
- package/src/resolution-session.ts +414 -0
- package/src/resolver.ts +282 -0
- package/src/unique-id.ts +24 -0
- package/src/value-promise.ts +318 -0
- package/index.d.ts +0 -6
- package/index.js +0 -9
- package/lib/index.d.ts +0 -1
- package/lib/index.js +0 -12
- package/lib/index.js.map +0 -1
- package/lib/src/binding.d.ts +0 -98
- package/lib/src/binding.js +0 -169
- package/lib/src/binding.js.map +0 -1
- package/lib/src/context.d.ts +0 -14
- package/lib/src/context.js +0 -122
- package/lib/src/context.js.map +0 -1
- package/lib/src/index.d.ts +0 -10
- package/lib/src/index.js +0 -25
- package/lib/src/index.js.map +0 -1
- package/lib/src/inject.d.ts +0 -60
- package/lib/src/inject.js +0 -112
- package/lib/src/inject.js.map +0 -1
- package/lib/src/is-promise.d.ts +0 -1
- package/lib/src/is-promise.js +0 -15
- package/lib/src/is-promise.js.map +0 -1
- package/lib/src/provider.d.ts +0 -31
- package/lib/src/provider.js.map +0 -1
- package/lib/src/reflect.d.ts +0 -38
- package/lib/src/reflect.js +0 -143
- package/lib/src/reflect.js.map +0 -1
- package/lib/src/resolver.d.ts +0 -30
- package/lib/src/resolver.js +0 -141
- package/lib/src/resolver.js.map +0 -1
- package/lib6/index.d.ts +0 -1
- package/lib6/index.js +0 -12
- package/lib6/index.js.map +0 -1
- package/lib6/src/binding.d.ts +0 -98
- package/lib6/src/binding.js +0 -169
- package/lib6/src/binding.js.map +0 -1
- package/lib6/src/context.d.ts +0 -14
- package/lib6/src/context.js +0 -122
- package/lib6/src/context.js.map +0 -1
- package/lib6/src/index.d.ts +0 -10
- package/lib6/src/index.js +0 -25
- package/lib6/src/index.js.map +0 -1
- package/lib6/src/inject.d.ts +0 -60
- package/lib6/src/inject.js +0 -112
- package/lib6/src/inject.js.map +0 -1
- package/lib6/src/is-promise.d.ts +0 -1
- package/lib6/src/is-promise.js +0 -15
- package/lib6/src/is-promise.js.map +0 -1
- package/lib6/src/provider.d.ts +0 -31
- package/lib6/src/provider.js.map +0 -1
- package/lib6/src/reflect.d.ts +0 -38
- package/lib6/src/reflect.js +0 -143
- package/lib6/src/reflect.js.map +0 -1
- package/lib6/src/resolver.d.ts +0 -30
- package/lib6/src/resolver.js +0 -141
- 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
|
+
}
|