1 |
|
2 |
|
3 |
|
4 |
|
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 |
|
50 |
|
51 | export class InterceptedInvocationContext extends InvocationContext {
|
52 | |
53 |
|
54 |
|
55 |
|
56 | getGlobalInterceptorBindingKeys(): string[] {
|
57 | let bindings: Readonly<Binding<Interceptor>>[] = this.findByTag(
|
58 | ContextTags.GLOBAL_INTERCEPTOR,
|
59 | );
|
60 | bindings = bindings.filter(binding =>
|
61 |
|
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 |
|
73 |
|
74 |
|
75 |
|
76 | private applicableTo(binding: Readonly<Binding<unknown>>) {
|
77 | const sourceType = this.source?.type;
|
78 |
|
79 | if (sourceType == null) return true;
|
80 | const allowedSource: string | string[] =
|
81 | binding.tagMap[ContextTags.GLOBAL_INTERCEPTOR_SOURCE];
|
82 | return (
|
83 |
|
84 | allowedSource == null ||
|
85 |
|
86 | allowedSource === sourceType ||
|
87 |
|
88 | (Array.isArray(allowedSource) && allowedSource.includes(sourceType))
|
89 | );
|
90 | }
|
91 |
|
92 | |
93 |
|
94 |
|
95 |
|
96 | private sortGlobalInterceptorBindings(
|
97 | bindings: Readonly<Binding<Interceptor>>[],
|
98 | ) {
|
99 |
|
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 |
|
113 |
|
114 |
|
115 |
|
116 |
|
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 |
|
131 | interceptors = mergeInterceptors(classInterceptors, interceptors);
|
132 | const globalInterceptors = this.getGlobalInterceptorBindingKeys();
|
133 |
|
134 | interceptors = mergeInterceptors(globalInterceptors, interceptors);
|
135 | debug('Interceptors for %s', this.targetName, interceptors);
|
136 | return interceptors;
|
137 | }
|
138 | }
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 | export function asGlobalInterceptor(group?: string): BindingTemplate {
|
146 | return binding => {
|
147 | binding
|
148 |
|
149 | .tag(ContextTags.GLOBAL_INTERCEPTOR)
|
150 |
|
151 | .tag({[ContextTags.NAMESPACE]: GLOBAL_INTERCEPTOR_NAMESPACE});
|
152 | if (group) binding.tag({[ContextTags.GLOBAL_INTERCEPTOR_GROUP]: group});
|
153 | };
|
154 | }
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
161 | export function globalInterceptor(group?: string, ...specs: BindingSpec[]) {
|
162 | return injectable(asGlobalInterceptor(group), ...specs);
|
163 | }
|
164 |
|
165 |
|
166 |
|
167 |
|
168 | export interface Interceptor extends GenericInterceptor<InvocationContext> {}
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 | export type InterceptorOrKey = GenericInterceptorOrKey<InvocationContext>;
|
175 |
|
176 |
|
177 |
|
178 |
|
179 | export const INTERCEPT_METHOD_KEY = MetadataAccessor.create<
|
180 | InterceptorOrKey[],
|
181 | MethodDecorator
|
182 | >('intercept:method');
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
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 |
|
206 | for (const i of interceptorsToApply) {
|
207 | if (appliedInterceptors.has(i)) {
|
208 | interceptorsToApply.delete(i);
|
209 | }
|
210 | }
|
211 |
|
212 | for (const i of appliedInterceptors) {
|
213 | interceptorsToApply.add(i);
|
214 | }
|
215 | return Array.from(interceptorsToApply);
|
216 | }
|
217 |
|
218 |
|
219 |
|
220 |
|
221 | export const INTERCEPT_CLASS_KEY = MetadataAccessor.create<
|
222 | InterceptorOrKey[],
|
223 | ClassDecorator
|
224 | >('intercept:class');
|
225 |
|
226 |
|
227 |
|
228 |
|
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 |
|
241 |
|
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 |
|
256 | ownMetadata[methodName] = mergeInterceptors(this.spec, interceptors);
|
257 |
|
258 | return ownMetadata;
|
259 | }
|
260 | }
|
261 |
|
262 |
|
263 |
|
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 | export function intercept(...interceptorOrKeys: InterceptorOrKey[]) {
|
284 | return function interceptDecoratorForClassOrMethod(
|
285 |
|
286 |
|
287 | target: any,
|
288 | method?: string,
|
289 |
|
290 |
|
291 |
|
292 | methodDescriptor?: TypedPropertyDescriptor<any>,
|
293 | ) {
|
294 | if (method && methodDescriptor) {
|
295 |
|
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 |
|
304 | return InterceptClassDecoratorFactory.createDecorator(
|
305 | INTERCEPT_CLASS_KEY,
|
306 | interceptorOrKeys,
|
307 | {decoratorName: '@intercept'},
|
308 | )(target);
|
309 | }
|
310 |
|
311 | throw new Error(
|
312 | '@intercept cannot be used on a property: ' +
|
313 | DecoratorFactory.getTargetName(target, method, methodDescriptor),
|
314 | );
|
315 | };
|
316 | }
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 | export function invokeMethodWithInterceptors(
|
327 | context: Context,
|
328 | target: object,
|
329 | methodName: string,
|
330 | args: InvocationArgs,
|
331 | options: InvocationOptions = {},
|
332 | ): ValueOrPromise<InvocationResult> {
|
333 |
|
334 |
|
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 |
|
359 |
|
360 | export interface InterceptorBindingOptions extends BindingFromClassOptions {
|
361 | |
362 |
|
363 |
|
364 | global?: boolean;
|
365 | |
366 |
|
367 |
|
368 | group?: string;
|
369 | |
370 |
|
371 |
|
372 | source?: string | string[];
|
373 | }
|
374 |
|
375 |
|
376 |
|
377 |
|
378 |
|
379 |
|
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 |
|
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 | }
|