1 | // Copyright IBM Corp. and LoopBack contributors 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 {Context} from './context';
|
7 | import {invokeMethodWithInterceptors} from './interceptor';
|
8 | import {InvocationArgs, InvocationSource} from './invocation';
|
9 | import {ResolutionSession} from './resolution-session';
|
10 | import {ValueOrPromise} from './value-promise';
|
11 |
|
12 | /**
|
13 | * Create the Promise type for `T`. If `T` extends `Promise`, the type is `T`,
|
14 | * otherwise the type is `ValueOrPromise<T>`.
|
15 | */
|
16 | export type AsValueOrPromise<T> =
|
17 | T extends Promise<unknown> ? T : ValueOrPromise<T>;
|
18 |
|
19 | /**
|
20 | * The intercepted variant of a function to return `ValueOrPromise<T>`.
|
21 | * If `T` is not a function, the type is `T`.
|
22 | */
|
23 | export type AsInterceptedFunction<T> = T extends (
|
24 | ...args: InvocationArgs
|
25 | ) => infer R
|
26 | ? (...args: Parameters<T>) => AsValueOrPromise<R>
|
27 | : T;
|
28 |
|
29 | /**
|
30 | * The proxy type for `T`. The return type for any method of `T` with original
|
31 | * return type `R` becomes `ValueOrPromise<R>` if `R` does not extend `Promise`.
|
32 | * Property types stay untouched.
|
33 | *
|
34 | * @example
|
35 | * ```ts
|
36 | * class MyController {
|
37 | * name: string;
|
38 | *
|
39 | * greet(name: string): string {
|
40 | * return `Hello, ${name}`;
|
41 | * }
|
42 | *
|
43 | * async hello(name: string) {
|
44 | * return `Hello, ${name}`;
|
45 | * }
|
46 | * }
|
47 | * ```
|
48 | *
|
49 | * `AsyncProxy<MyController>` will be:
|
50 | * ```ts
|
51 | * {
|
52 | * name: string; // the same as MyController
|
53 | * greet(name: string): ValueOrPromise<string>; // the return type becomes `ValueOrPromise<string>`
|
54 | * hello(name: string): Promise<string>; // the same as MyController
|
55 | * }
|
56 | * ```
|
57 | */
|
58 | export type AsyncProxy<T> = {[P in keyof T]: AsInterceptedFunction<T[P]>};
|
59 |
|
60 | /**
|
61 | * Invocation source for injected proxies. It wraps a snapshot of the
|
62 | * `ResolutionSession` that tracks the binding/injection stack.
|
63 | */
|
64 | export class ProxySource implements InvocationSource<ResolutionSession> {
|
65 | type = 'proxy';
|
66 | constructor(readonly value: ResolutionSession) {}
|
67 |
|
68 | toString() {
|
69 | return this.value.getBindingPath();
|
70 | }
|
71 | }
|
72 |
|
73 | /**
|
74 | * A proxy handler that applies interceptors
|
75 | *
|
76 | * See https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Proxy
|
77 | */
|
78 | export class InterceptionHandler<T extends object> implements ProxyHandler<T> {
|
79 | constructor(
|
80 | private context = new Context(),
|
81 | private session?: ResolutionSession,
|
82 | private source?: InvocationSource,
|
83 | ) {}
|
84 |
|
85 | get(target: T, propertyName: PropertyKey, receiver: unknown) {
|
86 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
87 | const targetObj = target as any;
|
88 | if (typeof propertyName !== 'string') return targetObj[propertyName];
|
89 | const propertyOrMethod = targetObj[propertyName];
|
90 | if (typeof propertyOrMethod === 'function') {
|
91 | return (...args: InvocationArgs) => {
|
92 | return invokeMethodWithInterceptors(
|
93 | this.context,
|
94 | target,
|
95 | propertyName,
|
96 | args,
|
97 | {
|
98 | source:
|
99 | this.source ?? (this.session && new ProxySource(this.session)),
|
100 | },
|
101 | );
|
102 | };
|
103 | } else {
|
104 | return propertyOrMethod;
|
105 | }
|
106 | }
|
107 | }
|
108 |
|
109 | /**
|
110 | * Create a proxy that applies interceptors for method invocations
|
111 | * @param target - Target class or object
|
112 | * @param context - Context object
|
113 | * @param session - Resolution session
|
114 | * @param source - Invocation source
|
115 | */
|
116 | export function createProxyWithInterceptors<T extends object>(
|
117 | target: T,
|
118 | context?: Context,
|
119 | session?: ResolutionSession,
|
120 | source?: InvocationSource,
|
121 | ): AsyncProxy<T> {
|
122 | return new Proxy(
|
123 | target,
|
124 | new InterceptionHandler(context, ResolutionSession.fork(session), source),
|
125 | ) as AsyncProxy<T>;
|
126 | }
|