UNPKG

3.76 kBPlain TextView Raw
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
6import {Context} from './context';
7import {invokeMethodWithInterceptors} from './interceptor';
8import {InvocationArgs, InvocationSource} from './invocation';
9import {ResolutionSession} from './resolution-session';
10import {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 */
16export 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 */
23export 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 */
58export 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 */
64export 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 */
78export 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 */
116export 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}