1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | import {DecoratorFactory} from '@loopback/metadata';
|
7 | import assert from 'assert';
|
8 | import debugFactory from 'debug';
|
9 | import {Context} from './context';
|
10 | import {invokeMethodWithInterceptors} from './interceptor';
|
11 | import {ResolutionSession} from './resolution-session';
|
12 | import {resolveInjectedArguments} from './resolver';
|
13 | import {transformValueOrPromise, ValueOrPromise} from './value-promise';
|
14 |
|
15 | const debug = debugFactory('loopback:context:invocation');
|
16 | const getTargetName = DecoratorFactory.getTargetName;
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 | export type InvocationResult = any;
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | export type InvocationArgs = any[];
|
29 |
|
30 |
|
31 |
|
32 |
|
33 | export interface InvocationSource<T = unknown> {
|
34 | |
35 |
|
36 |
|
37 | readonly type: string;
|
38 | |
39 |
|
40 |
|
41 | readonly value: T;
|
42 | }
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 |
|
49 | export class InvocationContext extends Context {
|
50 | |
51 |
|
52 |
|
53 |
|
54 |
|
55 |
|
56 |
|
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 |
|
70 |
|
71 | get targetClass() {
|
72 | return typeof this.target === 'function'
|
73 | ? this.target
|
74 | : this.target.constructor;
|
75 | }
|
76 |
|
77 | |
78 |
|
79 |
|
80 | get targetName() {
|
81 | return getTargetName(this.target, this.methodName);
|
82 | }
|
83 |
|
84 | |
85 |
|
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 |
|
98 |
|
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 |
|
111 |
|
112 |
|
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 |
|
138 |
|
139 | export type InvocationOptions = {
|
140 | |
141 |
|
142 |
|
143 | skipParameterInjection?: boolean;
|
144 | |
145 |
|
146 |
|
147 | skipInterceptors?: boolean;
|
148 | |
149 |
|
150 |
|
151 |
|
152 | source?: InvocationSource;
|
153 | |
154 |
|
155 |
|
156 | session?: ResolutionSession;
|
157 | };
|
158 |
|
159 |
|
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 | export 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 |
|
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 |
|
191 | return invokeMethodWithInterceptors(
|
192 | ctx,
|
193 | target,
|
194 | method,
|
195 | nonInjectedArgs,
|
196 | options,
|
197 | );
|
198 | }
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 | function 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 |
|
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 |
|
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 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 |
|
251 | function invokeTargetMethod(
|
252 | ctx: Context,
|
253 | target: object,
|
254 | methodName: string,
|
255 | args: InvocationArgs,
|
256 | ): InvocationResult {
|
257 | const targetWithMethods = target as Record<string, Function>;
|
258 |
|
259 | if (debug.enabled) {
|
260 | debug('Invoking method %s', getTargetName(target, methodName), args);
|
261 | }
|
262 |
|
263 | const result = targetWithMethods[methodName](...args);
|
264 |
|
265 | if (debug.enabled) {
|
266 | debug('Method invoked: %s', getTargetName(target, methodName), result);
|
267 | }
|
268 | return result;
|
269 | }
|