UNPKG

7.67 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 {DecoratorFactory} from '@loopback/metadata';
7import assert from 'assert';
8import debugFactory from 'debug';
9import {Context} from './context';
10import {invokeMethodWithInterceptors} from './interceptor';
11import {ResolutionSession} from './resolution-session';
12import {resolveInjectedArguments} from './resolver';
13import {transformValueOrPromise, ValueOrPromise} from './value-promise';
14
15const debug = debugFactory('loopback:context:invocation');
16const getTargetName = DecoratorFactory.getTargetName;
17
18/**
19 * Return value for a method invocation
20 */
21// eslint-disable-next-line @typescript-eslint/no-explicit-any
22export type InvocationResult = any;
23
24/**
25 * Array of arguments for a method invocation
26 */
27// eslint-disable-next-line @typescript-eslint/no-explicit-any
28export type InvocationArgs = any[];
29
30/**
31 * An interface to represent the caller of the invocation
32 */
33export 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 */
49export 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 */
139export 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 */
169export 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 */
208function 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 */
251function 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}