UNPKG

21 kBJavaScriptView Raw
1"use strict";
2// Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
3// Node module: @loopback/context
4// This file is licensed under the MIT License.
5// License text available at https://opensource.org/licenses/MIT
6Object.defineProperty(exports, "__esModule", { value: true });
7exports.hasInjections = exports.inspectInjections = exports.describeInjectedProperties = exports.inspectTargetType = exports.describeInjectedArguments = exports.assertTargetType = exports.Getter = exports.inject = void 0;
8const metadata_1 = require("@loopback/metadata");
9const binding_1 = require("./binding");
10const binding_filter_1 = require("./binding-filter");
11const context_view_1 = require("./context-view");
12const resolution_session_1 = require("./resolution-session");
13const INJECT_PARAMETERS_KEY = metadata_1.MetadataAccessor.create('inject:parameters');
14const INJECT_PROPERTIES_KEY = metadata_1.MetadataAccessor.create('inject:properties');
15// A key to cache described argument injections
16const INJECT_METHODS_KEY = metadata_1.MetadataAccessor.create('inject:methods');
17/**
18 * A decorator to annotate method arguments for automatic injection
19 * by LoopBack IoC container.
20 *
21 * @example
22 * Usage - Typescript:
23 *
24 * ```ts
25 * class InfoController {
26 * @inject('authentication.user') public userName: string;
27 *
28 * constructor(@inject('application.name') public appName: string) {
29 * }
30 * // ...
31 * }
32 * ```
33 *
34 * Usage - JavaScript:
35 *
36 * - TODO(bajtos)
37 *
38 * @param bindingSelector - What binding to use in order to resolve the value of the
39 * decorated constructor parameter or property.
40 * @param metadata - Optional metadata to help the injection
41 * @param resolve - Optional function to resolve the injection
42 *
43 */
44function inject(bindingSelector, metadata, resolve) {
45 if (typeof bindingSelector === 'function' && !resolve) {
46 resolve = resolveValuesByFilter;
47 }
48 const injectionMetadata = Object.assign({ decorator: '@inject' }, metadata);
49 if (injectionMetadata.bindingComparator && !resolve) {
50 throw new Error('Binding comparator is only allowed with a binding filter');
51 }
52 if (!bindingSelector && typeof resolve !== 'function') {
53 throw new Error('A non-empty binding selector or resolve function is required for @inject');
54 }
55 return function markParameterOrPropertyAsInjected(target, member, methodDescriptorOrParameterIndex) {
56 if (typeof methodDescriptorOrParameterIndex === 'number') {
57 // The decorator is applied to a method parameter
58 // Please note propertyKey is `undefined` for constructor
59 const paramDecorator = metadata_1.ParameterDecoratorFactory.createDecorator(INJECT_PARAMETERS_KEY, {
60 target,
61 member,
62 methodDescriptorOrParameterIndex,
63 bindingSelector,
64 metadata: injectionMetadata,
65 resolve,
66 },
67 // Do not deep clone the spec as only metadata is mutable and it's
68 // shallowly cloned
69 { cloneInputSpec: false, decoratorName: injectionMetadata.decorator });
70 paramDecorator(target, member, methodDescriptorOrParameterIndex);
71 }
72 else if (member) {
73 // Property or method
74 if (target instanceof Function) {
75 throw new Error('@inject is not supported for a static property: ' +
76 metadata_1.DecoratorFactory.getTargetName(target, member));
77 }
78 if (methodDescriptorOrParameterIndex) {
79 // Method
80 throw new Error('@inject cannot be used on a method: ' +
81 metadata_1.DecoratorFactory.getTargetName(target, member, methodDescriptorOrParameterIndex));
82 }
83 const propDecorator = metadata_1.PropertyDecoratorFactory.createDecorator(INJECT_PROPERTIES_KEY, {
84 target,
85 member,
86 methodDescriptorOrParameterIndex,
87 bindingSelector,
88 metadata: injectionMetadata,
89 resolve,
90 },
91 // Do not deep clone the spec as only metadata is mutable and it's
92 // shallowly cloned
93 { cloneInputSpec: false, decoratorName: injectionMetadata.decorator });
94 propDecorator(target, member);
95 }
96 else {
97 // It won't happen here as `@inject` is not compatible with ClassDecorator
98 /* istanbul ignore next */
99 throw new Error('@inject can only be used on a property or a method parameter');
100 }
101 };
102}
103exports.inject = inject;
104var Getter;
105(function (Getter) {
106 /**
107 * Convert a value into a Getter returning that value.
108 * @param value
109 */
110 function fromValue(value) {
111 return () => Promise.resolve(value);
112 }
113 Getter.fromValue = fromValue;
114})(Getter || (exports.Getter = Getter = {}));
115(function (inject) {
116 /**
117 * Inject a function for getting the actual bound value.
118 *
119 * This is useful when implementing Actions, where
120 * the action is instantiated for Sequence constructor, but some
121 * of action's dependencies become bound only after other actions
122 * have been executed by the sequence.
123 *
124 * See also `Getter<T>`.
125 *
126 * @param bindingSelector - The binding key or filter we want to eventually get
127 * value(s) from.
128 * @param metadata - Optional metadata to help the injection
129 */
130 inject.getter = function injectGetter(bindingSelector, metadata) {
131 metadata = Object.assign({ decorator: '@inject.getter' }, metadata);
132 return inject(bindingSelector, metadata, (0, binding_filter_1.isBindingAddress)(bindingSelector)
133 ? resolveAsGetter
134 : resolveAsGetterByFilter);
135 };
136 /**
137 * Inject a function for setting (binding) the given key to a given
138 * value. (Only static/constant values are supported, it's not possible
139 * to bind a key to a class or a provider.)
140 *
141 * This is useful e.g. when implementing Actions that are contributing
142 * new Elements.
143 *
144 * See also `Setter<T>`.
145 *
146 * @param bindingKey - The key of the value we want to set.
147 * @param metadata - Optional metadata to help the injection
148 */
149 inject.setter = function injectSetter(bindingKey, metadata) {
150 metadata = Object.assign({ decorator: '@inject.setter' }, metadata);
151 return inject(bindingKey, metadata, resolveAsSetter);
152 };
153 /**
154 * Inject the binding object for the given key. This is useful if a binding
155 * needs to be set up beyond just a constant value allowed by
156 * `@inject.setter`. The injected binding is found or created based on the
157 * `metadata.bindingCreation` option. See `BindingCreationPolicy` for more
158 * details.
159 *
160 * @example
161 *
162 * ```ts
163 * class MyAuthAction {
164 * @inject.binding('current-user', {
165 * bindingCreation: BindingCreationPolicy.ALWAYS_CREATE,
166 * })
167 * private userBinding: Binding<UserProfile>;
168 *
169 * async authenticate() {
170 * this.userBinding.toDynamicValue(() => {...});
171 * }
172 * }
173 * ```
174 *
175 * @param bindingKey - Binding key
176 * @param metadata - Metadata for the injection
177 */
178 inject.binding = function injectBinding(bindingKey, metadata) {
179 metadata = Object.assign({ decorator: '@inject.binding' }, metadata);
180 return inject(bindingKey !== null && bindingKey !== void 0 ? bindingKey : '', metadata, resolveAsBinding);
181 };
182 /**
183 * Inject an array of values by a tag pattern string or regexp
184 *
185 * @example
186 * ```ts
187 * class AuthenticationManager {
188 * constructor(
189 * @inject.tag('authentication.strategy') public strategies: Strategy[],
190 * ) {}
191 * }
192 * ```
193 * @param bindingTag - Tag name, regex or object
194 * @param metadata - Optional metadata to help the injection
195 */
196 inject.tag = function injectByTag(bindingTag, metadata) {
197 metadata = Object.assign({ decorator: '@inject.tag', tag: bindingTag }, metadata);
198 return inject((0, binding_filter_1.filterByTag)(bindingTag), metadata);
199 };
200 /**
201 * Inject matching bound values by the filter function
202 *
203 * @example
204 * ```ts
205 * class MyControllerWithView {
206 * @inject.view(filterByTag('foo'))
207 * view: ContextView<string[]>;
208 * }
209 * ```
210 * @param bindingFilter - A binding filter function
211 * @param metadata
212 */
213 inject.view = function injectContextView(bindingFilter, metadata) {
214 metadata = Object.assign({ decorator: '@inject.view' }, metadata);
215 return inject(bindingFilter, metadata, resolveAsContextView);
216 };
217 /**
218 * Inject the context object.
219 *
220 * @example
221 * ```ts
222 * class MyProvider {
223 * constructor(@inject.context() private ctx: Context) {}
224 * }
225 * ```
226 */
227 inject.context = function injectContext() {
228 return inject('', { decorator: '@inject.context' }, (ctx) => ctx);
229 };
230})(inject || (exports.inject = inject = {}));
231/**
232 * Assert the target type inspected from TypeScript for injection to be the
233 * expected type. If the types don't match, an error is thrown.
234 * @param injection - Injection information
235 * @param expectedType - Expected type
236 * @param expectedTypeName - Name of the expected type to be used in the error
237 * @returns The name of the target
238 */
239function assertTargetType(injection, expectedType, expectedTypeName) {
240 const targetName = resolution_session_1.ResolutionSession.describeInjection(injection).targetName;
241 const targetType = inspectTargetType(injection);
242 if (targetType && targetType !== expectedType) {
243 expectedTypeName = expectedTypeName !== null && expectedTypeName !== void 0 ? expectedTypeName : expectedType.name;
244 throw new Error(`The type of ${targetName} (${targetType.name}) is not ${expectedTypeName}`);
245 }
246 return targetName;
247}
248exports.assertTargetType = assertTargetType;
249/**
250 * Resolver for `@inject.getter`
251 * @param ctx
252 * @param injection
253 * @param session
254 */
255function resolveAsGetter(ctx, injection, session) {
256 assertTargetType(injection, Function, 'Getter function');
257 const bindingSelector = injection.bindingSelector;
258 const options = {
259 // https://github.com/loopbackio/loopback-next/issues/9041
260 // We should start with a new session for `getter` resolution to avoid
261 // possible circular dependencies
262 session: undefined,
263 ...injection.metadata,
264 };
265 return function getter() {
266 return ctx.get(bindingSelector, options);
267 };
268}
269/**
270 * Resolver for `@inject.setter`
271 * @param ctx
272 * @param injection
273 */
274function resolveAsSetter(ctx, injection) {
275 const targetName = assertTargetType(injection, Function, 'Setter function');
276 const bindingSelector = injection.bindingSelector;
277 if (!(0, binding_filter_1.isBindingAddress)(bindingSelector)) {
278 throw new Error(`@inject.setter (${targetName}) does not allow BindingFilter.`);
279 }
280 if (bindingSelector === '') {
281 throw new Error('Binding key is not set for @inject.setter');
282 }
283 // No resolution session should be propagated into the setter
284 return function setter(value) {
285 const binding = findOrCreateBindingForInjection(ctx, injection);
286 binding.to(value);
287 };
288}
289function resolveAsBinding(ctx, injection, session) {
290 const targetName = assertTargetType(injection, binding_1.Binding);
291 const bindingSelector = injection.bindingSelector;
292 if (!(0, binding_filter_1.isBindingAddress)(bindingSelector)) {
293 throw new Error(`@inject.binding (${targetName}) does not allow BindingFilter.`);
294 }
295 return findOrCreateBindingForInjection(ctx, injection, session);
296}
297function findOrCreateBindingForInjection(ctx, injection, session) {
298 if (injection.bindingSelector === '')
299 return session === null || session === void 0 ? void 0 : session.currentBinding;
300 const bindingCreation = injection.metadata &&
301 injection.metadata.bindingCreation;
302 const binding = ctx.findOrCreateBinding(injection.bindingSelector, bindingCreation);
303 return binding;
304}
305/**
306 * Check if constructor injection should be applied to the base class
307 * of the given target class
308 *
309 * @param targetClass - Target class
310 */
311function shouldSkipBaseConstructorInjection(targetClass) {
312 // FXIME(rfeng): We use the class definition to check certain patterns
313 const classDef = targetClass.toString();
314 return (
315 /*
316 * See https://github.com/loopbackio/loopback-next/issues/2946
317 * A class decorator can return a new constructor that mixes in
318 * additional properties/methods.
319 *
320 * @example
321 * ```ts
322 * class extends baseConstructor {
323 * // The constructor calls `super(...arguments)`
324 * constructor() {
325 * super(...arguments);
326 * }
327 * classProperty = 'a classProperty';
328 * classFunction() {
329 * return 'a classFunction';
330 * }
331 * };
332 * ```
333 *
334 * We check the following pattern:
335 * ```ts
336 * constructor() {
337 * super(...arguments);
338 * }
339 * ```
340 */
341 !classDef.match(/\s+constructor\s*\(\s*\)\s*\{\s*super\(\.\.\.arguments\)/) &&
342 /*
343 * See https://github.com/loopbackio/loopback-next/issues/1565
344 *
345 * @example
346 * ```ts
347 * class BaseClass {
348 * constructor(@inject('foo') protected foo: string) {}
349 * // ...
350 * }
351 *
352 * class SubClass extends BaseClass {
353 * // No explicit constructor is present
354 *
355 * @inject('bar')
356 * private bar: number;
357 * // ...
358 * };
359 *
360 */
361 classDef.match(/\s+constructor\s*\([^\)]*\)\s+\{/m));
362}
363/**
364 * Return an array of injection objects for parameters
365 * @param target - The target class for constructor or static methods,
366 * or the prototype for instance methods
367 * @param method - Method name, undefined for constructor
368 */
369function describeInjectedArguments(target, method) {
370 var _a, _b;
371 method = method !== null && method !== void 0 ? method : '';
372 // Try to read from cache
373 const cache = (_a = metadata_1.MetadataInspector.getAllMethodMetadata(INJECT_METHODS_KEY, target, {
374 ownMetadataOnly: true,
375 })) !== null && _a !== void 0 ? _a : {};
376 let meta = cache[method];
377 if (meta)
378 return meta;
379 // Build the description
380 const options = {};
381 if (method === '') {
382 if (shouldSkipBaseConstructorInjection(target)) {
383 options.ownMetadataOnly = true;
384 }
385 }
386 else if (Object.prototype.hasOwnProperty.call(target, method)) {
387 // The method exists in the target, no injections on the super method
388 // should be honored
389 options.ownMetadataOnly = true;
390 }
391 meta =
392 (_b = metadata_1.MetadataInspector.getAllParameterMetadata(INJECT_PARAMETERS_KEY, target, method, options)) !== null && _b !== void 0 ? _b : [];
393 // Cache the result
394 cache[method] = meta;
395 metadata_1.MetadataInspector.defineMetadata(INJECT_METHODS_KEY, cache, target);
396 return meta;
397}
398exports.describeInjectedArguments = describeInjectedArguments;
399/**
400 * Inspect the target type for the injection to find out the corresponding
401 * JavaScript type
402 * @param injection - Injection information
403 */
404function inspectTargetType(injection) {
405 var _a;
406 if (typeof injection.methodDescriptorOrParameterIndex === 'number') {
407 const designType = metadata_1.MetadataInspector.getDesignTypeForMethod(injection.target, injection.member);
408 return (_a = designType === null || designType === void 0 ? void 0 : designType.parameterTypes) === null || _a === void 0 ? void 0 : _a[injection.methodDescriptorOrParameterIndex];
409 }
410 return metadata_1.MetadataInspector.getDesignTypeForProperty(injection.target, injection.member);
411}
412exports.inspectTargetType = inspectTargetType;
413/**
414 * Resolve an array of bound values matching the filter function for `@inject`.
415 * @param ctx - Context object
416 * @param injection - Injection information
417 * @param session - Resolution session
418 */
419function resolveValuesByFilter(ctx, injection, session) {
420 assertTargetType(injection, Array);
421 const bindingFilter = injection.bindingSelector;
422 const view = new context_view_1.ContextView(ctx, bindingFilter, injection.metadata.bindingComparator);
423 return view.resolve(session);
424}
425/**
426 * Resolve to a getter function that returns an array of bound values matching
427 * the filter function for `@inject.getter`.
428 *
429 * @param ctx - Context object
430 * @param injection - Injection information
431 * @param session - Resolution session
432 */
433function resolveAsGetterByFilter(ctx, injection, session) {
434 assertTargetType(injection, Function, 'Getter function');
435 const bindingFilter = injection.bindingSelector;
436 return (0, context_view_1.createViewGetter)(ctx, bindingFilter, injection.metadata.bindingComparator, session);
437}
438/**
439 * Resolve to an instance of `ContextView` by the binding filter function
440 * for `@inject.view`
441 * @param ctx - Context object
442 * @param injection - Injection information
443 */
444function resolveAsContextView(ctx, injection) {
445 assertTargetType(injection, context_view_1.ContextView);
446 const bindingFilter = injection.bindingSelector;
447 const view = new context_view_1.ContextView(ctx, bindingFilter, injection.metadata.bindingComparator);
448 view.open();
449 return view;
450}
451/**
452 * Return a map of injection objects for properties
453 * @param target - The target class for static properties or
454 * prototype for instance properties.
455 */
456function describeInjectedProperties(target) {
457 var _a;
458 const metadata = (_a = metadata_1.MetadataInspector.getAllPropertyMetadata(INJECT_PROPERTIES_KEY, target)) !== null && _a !== void 0 ? _a : {};
459 return metadata;
460}
461exports.describeInjectedProperties = describeInjectedProperties;
462/**
463 * Inspect injections for a binding created with `toClass` or `toProvider`
464 * @param binding - Binding object
465 */
466function inspectInjections(binding) {
467 var _a;
468 const json = {};
469 const ctor = (_a = binding.valueConstructor) !== null && _a !== void 0 ? _a : binding.providerConstructor;
470 if (ctor == null)
471 return json;
472 const constructorInjections = describeInjectedArguments(ctor, '').map(inspectInjection);
473 if (constructorInjections.length) {
474 json.constructorArguments = constructorInjections;
475 }
476 const propertyInjections = describeInjectedProperties(ctor.prototype);
477 const properties = {};
478 for (const p in propertyInjections) {
479 properties[p] = inspectInjection(propertyInjections[p]);
480 }
481 if (Object.keys(properties).length) {
482 json.properties = properties;
483 }
484 return json;
485}
486exports.inspectInjections = inspectInjections;
487/**
488 * Inspect an injection
489 * @param injection - Injection information
490 */
491function inspectInjection(injection) {
492 var _a, _b;
493 const injectionInfo = resolution_session_1.ResolutionSession.describeInjection(injection);
494 const descriptor = {};
495 if (injectionInfo.targetName) {
496 descriptor.targetName = injectionInfo.targetName;
497 }
498 if ((0, binding_filter_1.isBindingAddress)(injectionInfo.bindingSelector)) {
499 // Binding key
500 descriptor.bindingKey = injectionInfo.bindingSelector.toString();
501 }
502 else if ((0, binding_filter_1.isBindingTagFilter)(injectionInfo.bindingSelector)) {
503 // Binding tag filter
504 descriptor.bindingTagPattern = JSON.parse(JSON.stringify(injectionInfo.bindingSelector.bindingTagPattern));
505 }
506 else {
507 // Binding filter function
508 descriptor.bindingFilter =
509 (_b = (_a = injectionInfo.bindingSelector) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : '<function>';
510 }
511 // Inspect metadata
512 if (injectionInfo.metadata) {
513 if (injectionInfo.metadata.decorator &&
514 injectionInfo.metadata.decorator !== '@inject') {
515 descriptor.decorator = injectionInfo.metadata.decorator;
516 }
517 if (injectionInfo.metadata.optional) {
518 descriptor.optional = injectionInfo.metadata.optional;
519 }
520 }
521 return descriptor;
522}
523/**
524 * Check if the given class has `@inject` or other decorations that map to
525 * `@inject`.
526 *
527 * @param cls - Class with possible `@inject` decorations
528 */
529function hasInjections(cls) {
530 return (metadata_1.MetadataInspector.getClassMetadata(INJECT_PARAMETERS_KEY, cls) != null ||
531 metadata_1.Reflector.getMetadata(INJECT_PARAMETERS_KEY.toString(), cls.prototype) !=
532 null ||
533 metadata_1.MetadataInspector.getAllPropertyMetadata(INJECT_PROPERTIES_KEY, cls.prototype) != null);
534}
535exports.hasInjections = hasInjections;
536//# sourceMappingURL=inject.js.map
\No newline at end of file