@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.
- package/CHANGELOG.md +81 -0
- package/dist/binding-config.js +1 -1
- package/dist/binding-config.js.map +1 -1
- package/dist/binding-filter.d.ts +19 -1
- package/dist/binding-filter.js +40 -7
- package/dist/binding-filter.js.map +1 -1
- package/dist/binding-inspector.js +12 -11
- package/dist/binding-inspector.js.map +1 -1
- package/dist/binding-sorter.js +2 -2
- package/dist/binding-sorter.js.map +1 -1
- package/dist/binding.d.ts +42 -4
- package/dist/binding.js +40 -10
- package/dist/binding.js.map +1 -1
- 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 +1 -36
- package/dist/context-subscription.d.ts +147 -0
- package/dist/context-subscription.js +336 -0
- package/dist/context-subscription.js.map +1 -0
- package/dist/context-tag-indexer.d.ts +42 -0
- package/dist/context-tag-indexer.js +134 -0
- package/dist/context-tag-indexer.js.map +1 -0
- package/dist/context-view.d.ts +2 -1
- package/dist/context-view.js +5 -2
- package/dist/context-view.js.map +1 -1
- package/dist/context.d.ts +35 -66
- package/dist/context.js +78 -250
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/inject-config.js +3 -3
- package/dist/inject-config.js.map +1 -1
- package/dist/inject.d.ts +2 -2
- package/dist/inject.js +18 -11
- package/dist/inject.js.map +1 -1
- package/dist/interception-proxy.d.ts +15 -3
- package/dist/interception-proxy.js +20 -4
- package/dist/interception-proxy.js.map +1 -1
- package/dist/interceptor-chain.js +5 -2
- package/dist/interceptor-chain.js.map +1 -1
- package/dist/interceptor.d.ts +6 -0
- package/dist/interceptor.js +38 -12
- package/dist/interceptor.js.map +1 -1
- package/dist/invocation.d.ts +20 -2
- package/dist/invocation.js +14 -12
- package/dist/invocation.js.map +1 -1
- package/dist/keys.d.ts +6 -0
- package/dist/keys.js +6 -0
- package/dist/keys.js.map +1 -1
- package/dist/resolution-session.d.ts +1 -0
- package/dist/resolution-session.js +13 -6
- package/dist/resolution-session.js.map +1 -1
- package/dist/resolver.js +13 -8
- package/dist/resolver.js.map +1 -1
- package/dist/value-promise.d.ts +1 -3
- package/package.json +9 -9
- package/src/binding-config.ts +1 -1
- package/src/binding-filter.ts +61 -9
- package/src/binding-inspector.ts +6 -8
- package/src/binding-sorter.ts +2 -2
- package/src/binding.ts +73 -9
- package/src/context-event.ts +30 -0
- package/src/context-observer.ts +1 -38
- package/src/context-subscription.ts +403 -0
- package/src/context-tag-indexer.ts +149 -0
- package/src/context-view.ts +3 -6
- package/src/context.ts +94 -293
- package/src/index.ts +5 -3
- package/src/inject-config.ts +3 -3
- package/src/inject.ts +19 -10
- package/src/interception-proxy.ts +25 -3
- package/src/interceptor-chain.ts +1 -1
- package/src/interceptor.ts +34 -8
- package/src/invocation.ts +26 -7
- package/src/keys.ts +7 -0
- package/src/resolution-session.ts +9 -5
- package/src/resolver.ts +5 -5
- 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(
|
|
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(
|
|
116
|
+
return new Proxy(
|
|
117
|
+
target,
|
|
118
|
+
new InterceptionHandler(context, ResolutionSession.fork(session)),
|
|
119
|
+
) as AsyncProxy<T>;
|
|
98
120
|
}
|
package/src/interceptor-chain.ts
CHANGED
|
@@ -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
|
|
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';
|
package/src/interceptor.ts
CHANGED
|
@@ -11,11 +11,10 @@ import {
|
|
|
11
11
|
MetadataMap,
|
|
12
12
|
MethodDecoratorFactory,
|
|
13
13
|
} from '@loopback/metadata';
|
|
14
|
-
import
|
|
15
|
-
import
|
|
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
|
-
|
|
51
|
-
|
|
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
|
|
8
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
8
|
-
import
|
|
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
|
|
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
|
|
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
|
|
package/src/value-promise.ts
CHANGED