UNPKG

9.17 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2017,2019. 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 debugModule from 'debug';
9import {isBindingAddress} from './binding-filter';
10import {BindingAddress} from './binding-key';
11import {Context} from './context';
12import {
13 describeInjectedArguments,
14 describeInjectedProperties,
15 Injection,
16} from './inject';
17import {
18 ResolutionError,
19 ResolutionOptions,
20 ResolutionSession,
21} from './resolution-session';
22import {
23 BoundValue,
24 Constructor,
25 MapObject,
26 resolveList,
27 resolveMap,
28 transformValueOrPromise,
29 ValueOrPromise,
30} from './value-promise';
31
32const debug = debugModule('loopback:context:resolver');
33const getTargetName = DecoratorFactory.getTargetName;
34
35/**
36 * Create an instance of a class which constructor has arguments
37 * decorated with `@inject`.
38 *
39 * The function returns a class when all dependencies were
40 * resolved synchronously, or a Promise otherwise.
41 *
42 * @param ctor - The class constructor to call.
43 * @param ctx - The context containing values for `@inject` resolution
44 * @param session - Optional session for binding and dependency resolution
45 * @param nonInjectedArgs - Optional array of args for non-injected parameters
46 */
47export function instantiateClass<T extends object>(
48 ctor: Constructor<T>,
49 ctx: Context,
50 session?: ResolutionSession,
51 // eslint-disable-next-line @typescript-eslint/no-explicit-any
52 nonInjectedArgs?: any[],
53): ValueOrPromise<T> {
54 /* istanbul ignore if */
55 if (debug.enabled) {
56 debug('Instantiating %s', getTargetName(ctor));
57 if (nonInjectedArgs?.length) {
58 debug('Non-injected arguments:', nonInjectedArgs);
59 }
60 }
61 const argsOrPromise = resolveInjectedArguments(
62 ctor,
63 '',
64 ctx,
65 session,
66 nonInjectedArgs,
67 );
68 const propertiesOrPromise = resolveInjectedProperties(ctor, ctx, session);
69 const inst: ValueOrPromise<T> = transformValueOrPromise(
70 argsOrPromise,
71 args => {
72 /* istanbul ignore if */
73 if (debug.enabled) {
74 debug('Injected arguments for %s():', ctor.name, args);
75 }
76 return new ctor(...args);
77 },
78 );
79 return transformValueOrPromise(propertiesOrPromise, props => {
80 /* istanbul ignore if */
81 if (debug.enabled) {
82 debug('Injected properties for %s:', ctor.name, props);
83 }
84 return transformValueOrPromise<T, T>(inst, obj =>
85 Object.assign(obj, props),
86 );
87 });
88}
89
90/**
91 * If the scope of current binding is `SINGLETON`, reset the context
92 * to be the one that owns the current binding to make sure a singleton
93 * does not have dependencies injected from child contexts unless the
94 * injection is for method (excluding constructor) parameters.
95 */
96function resolveContext(
97 ctx: Context,
98 injection: Readonly<Injection>,
99 session?: ResolutionSession,
100) {
101 const currentBinding = session?.currentBinding;
102 if (currentBinding == null) {
103 // No current binding
104 return ctx;
105 }
106
107 const isConstructorOrPropertyInjection =
108 // constructor injection
109 !injection.member ||
110 // property injection
111 typeof injection.methodDescriptorOrParameterIndex !== 'number';
112
113 if (isConstructorOrPropertyInjection) {
114 // Set context to the resolution context of the current binding for
115 // constructor or property injections against a singleton
116 ctx = ctx.getResolutionContext(currentBinding)!;
117 }
118 return ctx;
119}
120
121/**
122 * Resolve the value or promise for a given injection
123 * @param ctx - Context
124 * @param injection - Descriptor of the injection
125 * @param session - Optional session for binding and dependency resolution
126 */
127function resolve<T>(
128 ctx: Context,
129 injection: Readonly<Injection>,
130 session?: ResolutionSession,
131): ValueOrPromise<T> {
132 /* istanbul ignore if */
133 if (debug.enabled) {
134 debug(
135 'Resolving an injection:',
136 ResolutionSession.describeInjection(injection),
137 );
138 }
139
140 ctx = resolveContext(ctx, injection, session);
141 const resolved = ResolutionSession.runWithInjection(
142 s => {
143 if (injection.resolve) {
144 // A custom resolve function is provided
145 return injection.resolve(ctx, injection, s);
146 } else {
147 // Default to resolve the value from the context by binding key
148 assert(
149 isBindingAddress(injection.bindingSelector),
150 'The binding selector must be an address (string or BindingKey)',
151 );
152 const key = injection.bindingSelector as BindingAddress;
153 const options: ResolutionOptions = {
154 session: s,
155 ...injection.metadata,
156 };
157 return ctx.getValueOrPromise(key, options);
158 }
159 },
160 injection,
161 session,
162 );
163 return resolved;
164}
165
166/**
167 * Given a function with arguments decorated with `@inject`,
168 * return the list of arguments resolved using the values
169 * bound in `ctx`.
170
171 * The function returns an argument array when all dependencies were
172 * resolved synchronously, or a Promise otherwise.
173 *
174 * @param target - The class for constructor injection or prototype for method
175 * injection
176 * @param method - The method name. If set to '', the constructor will
177 * be used.
178 * @param ctx - The context containing values for `@inject` resolution
179 * @param session - Optional session for binding and dependency resolution
180 * @param nonInjectedArgs - Optional array of args for non-injected parameters
181 */
182export function resolveInjectedArguments(
183 target: object,
184 method: string,
185 ctx: Context,
186 session?: ResolutionSession,
187 // eslint-disable-next-line @typescript-eslint/no-explicit-any
188 nonInjectedArgs?: any[],
189): ValueOrPromise<BoundValue[]> {
190 /* istanbul ignore if */
191 if (debug.enabled) {
192 debug('Resolving injected arguments for %s', getTargetName(target, method));
193 }
194 const targetWithMethods = <{[method: string]: Function}>target;
195 if (method) {
196 assert(
197 typeof targetWithMethods[method] === 'function',
198 `Method ${method} not found`,
199 );
200 }
201 // NOTE: the array may be sparse, i.e.
202 // Object.keys(injectedArgs).length !== injectedArgs.length
203 // Example value:
204 // [ , 'key1', , 'key2']
205 const injectedArgs = describeInjectedArguments(target, method);
206 const extraArgs = nonInjectedArgs ?? [];
207
208 let argLength = DecoratorFactory.getNumberOfParameters(target, method);
209
210 // Please note `injectedArgs` contains `undefined` for non-injected args
211 const numberOfInjected = injectedArgs.filter(i => i != null).length;
212 if (argLength < numberOfInjected + extraArgs.length) {
213 /**
214 * `Function.prototype.length` excludes the rest parameter and only includes
215 * parameters before the first one with a default value. For example,
216 * `hello(@inject('name') name: string = 'John')` gives 0 for argLength
217 */
218 argLength = numberOfInjected + extraArgs.length;
219 }
220
221 let nonInjectedIndex = 0;
222 return resolveList(new Array(argLength), (val, ix) => {
223 // The `val` argument is not used as the resolver only uses `injectedArgs`
224 // and `extraArgs` to return the new value
225 const injection = ix < injectedArgs.length ? injectedArgs[ix] : undefined;
226 if (
227 injection == null ||
228 (!injection.bindingSelector && !injection.resolve)
229 ) {
230 if (nonInjectedIndex < extraArgs.length) {
231 // Set the argument from the non-injected list
232 return extraArgs[nonInjectedIndex++];
233 } else {
234 const name = getTargetName(target, method, ix);
235 throw new ResolutionError(
236 `The argument '${name}' is not decorated for dependency injection ` +
237 'but no value was supplied by the caller. Did you forget to apply ' +
238 '@inject() to the argument?',
239 {context: ctx, options: {session}},
240 );
241 }
242 }
243
244 return resolve(
245 ctx,
246 injection,
247 // Clone the session so that multiple arguments can be resolved in parallel
248 ResolutionSession.fork(session),
249 );
250 });
251}
252
253/**
254 * Given a class with properties decorated with `@inject`,
255 * return the map of properties resolved using the values
256 * bound in `ctx`.
257
258 * The function returns an argument array when all dependencies were
259 * resolved synchronously, or a Promise otherwise.
260 *
261 * @param constructor - The class for which properties should be resolved.
262 * @param ctx - The context containing values for `@inject` resolution
263 * @param session - Optional session for binding and dependency resolution
264 */
265export function resolveInjectedProperties(
266 constructor: Function,
267 ctx: Context,
268 session?: ResolutionSession,
269): ValueOrPromise<MapObject<BoundValue>> {
270 /* istanbul ignore if */
271 if (debug.enabled) {
272 debug('Resolving injected properties for %s', getTargetName(constructor));
273 }
274 const injectedProperties = describeInjectedProperties(constructor.prototype);
275
276 return resolveMap(injectedProperties, injection =>
277 resolve(
278 ctx,
279 injection,
280 // Clone the session so that multiple properties can be resolved in parallel
281 ResolutionSession.fork(session),
282 ),
283 );
284}