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 || (exports.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 || (exports.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 // We need to clone the session for the getter as it will be resolved later
259 const forkedSession = resolution_session_1.ResolutionSession.fork(session);
260 const options = {
261 session: forkedSession,
262 ...injection.metadata,
263 };
264 return function getter() {
265 return ctx.get(bindingSelector, options);
266 };
267}
268/**
269 * Resolver for `@inject.setter`
270 * @param ctx
271 * @param injection
272 */
273function resolveAsSetter(ctx, injection) {
274 const targetName = assertTargetType(injection, Function, 'Setter function');
275 const bindingSelector = injection.bindingSelector;
276 if (!(0, binding_filter_1.isBindingAddress)(bindingSelector)) {
277 throw new Error(`@inject.setter (${targetName}) does not allow BindingFilter.`);
278 }
279 if (bindingSelector === '') {
280 throw new Error('Binding key is not set for @inject.setter');
281 }
282 // No resolution session should be propagated into the setter
283 return function setter(value) {
284 const binding = findOrCreateBindingForInjection(ctx, injection);
285 binding.to(value);
286 };
287}
288function resolveAsBinding(ctx, injection, session) {
289 const targetName = assertTargetType(injection, binding_1.Binding);
290 const bindingSelector = injection.bindingSelector;
291 if (!(0, binding_filter_1.isBindingAddress)(bindingSelector)) {
292 throw new Error(`@inject.binding (${targetName}) does not allow BindingFilter.`);
293 }
294 return findOrCreateBindingForInjection(ctx, injection, session);
295}
296function findOrCreateBindingForInjection(ctx, injection, session) {
297 if (injection.bindingSelector === '')
298 return session === null || session === void 0 ? void 0 : session.currentBinding;
299 const bindingCreation = injection.metadata &&
300 injection.metadata.bindingCreation;
301 const binding = ctx.findOrCreateBinding(injection.bindingSelector, bindingCreation);
302 return binding;
303}
304/**
305 * Check if constructor injection should be applied to the base class
306 * of the given target class
307 *
308 * @param targetClass - Target class
309 */
310function shouldSkipBaseConstructorInjection(targetClass) {
311 // FXIME(rfeng): We use the class definition to check certain patterns
312 const classDef = targetClass.toString();
313 return (
314 /*
315 * See https://github.com/loopbackio/loopback-next/issues/2946
316 * A class decorator can return a new constructor that mixes in
317 * additional properties/methods.
318 *
319 * @example
320 * ```ts
321 * class extends baseConstructor {
322 * // The constructor calls `super(...arguments)`
323 * constructor() {
324 * super(...arguments);
325 * }
326 * classProperty = 'a classProperty';
327 * classFunction() {
328 * return 'a classFunction';
329 * }
330 * };
331 * ```
332 *
333 * We check the following pattern:
334 * ```ts
335 * constructor() {
336 * super(...arguments);
337 * }
338 * ```
339 */
340 !classDef.match(/\s+constructor\s*\(\s*\)\s*\{\s*super\(\.\.\.arguments\)/) &&
341 /*
342 * See https://github.com/loopbackio/loopback-next/issues/1565
343 *
344 * @example
345 * ```ts
346 * class BaseClass {
347 * constructor(@inject('foo') protected foo: string) {}
348 * // ...
349 * }
350 *
351 * class SubClass extends BaseClass {
352 * // No explicit constructor is present
353 *
354 * @inject('bar')
355 * private bar: number;
356 * // ...
357 * };
358 *
359 */
360 classDef.match(/\s+constructor\s*\([^\)]*\)\s+\{/m));
361}
362/**
363 * Return an array of injection objects for parameters
364 * @param target - The target class for constructor or static methods,
365 * or the prototype for instance methods
366 * @param method - Method name, undefined for constructor
367 */
368function describeInjectedArguments(target, method) {
369 var _a, _b;
370 method = method !== null && method !== void 0 ? method : '';
371 // Try to read from cache
372 const cache = (_a = metadata_1.MetadataInspector.getAllMethodMetadata(INJECT_METHODS_KEY, target, {
373 ownMetadataOnly: true,
374 })) !== null && _a !== void 0 ? _a : {};
375 let meta = cache[method];
376 if (meta)
377 return meta;
378 // Build the description
379 const options = {};
380 if (method === '') {
381 if (shouldSkipBaseConstructorInjection(target)) {
382 options.ownMetadataOnly = true;
383 }
384 }
385 else if (Object.prototype.hasOwnProperty.call(target, method)) {
386 // The method exists in the target, no injections on the super method
387 // should be honored
388 options.ownMetadataOnly = true;
389 }
390 meta =
391 (_b = metadata_1.MetadataInspector.getAllParameterMetadata(INJECT_PARAMETERS_KEY, target, method, options)) !== null && _b !== void 0 ? _b : [];
392 // Cache the result
393 cache[method] = meta;
394 metadata_1.MetadataInspector.defineMetadata(INJECT_METHODS_KEY, cache, target);
395 return meta;
396}
397exports.describeInjectedArguments = describeInjectedArguments;
398/**
399 * Inspect the target type for the injection to find out the corresponding
400 * JavaScript type
401 * @param injection - Injection information
402 */
403function inspectTargetType(injection) {
404 var _a;
405 if (typeof injection.methodDescriptorOrParameterIndex === 'number') {
406 const designType = metadata_1.MetadataInspector.getDesignTypeForMethod(injection.target, injection.member);
407 return (_a = designType === null || designType === void 0 ? void 0 : designType.parameterTypes) === null || _a === void 0 ? void 0 : _a[injection.methodDescriptorOrParameterIndex];
408 }
409 return metadata_1.MetadataInspector.getDesignTypeForProperty(injection.target, injection.member);
410}
411exports.inspectTargetType = inspectTargetType;
412/**
413 * Resolve an array of bound values matching the filter function for `@inject`.
414 * @param ctx - Context object
415 * @param injection - Injection information
416 * @param session - Resolution session
417 */
418function resolveValuesByFilter(ctx, injection, session) {
419 assertTargetType(injection, Array);
420 const bindingFilter = injection.bindingSelector;
421 const view = new context_view_1.ContextView(ctx, bindingFilter, injection.metadata.bindingComparator);
422 return view.resolve(session);
423}
424/**
425 * Resolve to a getter function that returns an array of bound values matching
426 * the filter function for `@inject.getter`.
427 *
428 * @param ctx - Context object
429 * @param injection - Injection information
430 * @param session - Resolution session
431 */
432function resolveAsGetterByFilter(ctx, injection, session) {
433 assertTargetType(injection, Function, 'Getter function');
434 const bindingFilter = injection.bindingSelector;
435 return (0, context_view_1.createViewGetter)(ctx, bindingFilter, injection.metadata.bindingComparator, session);
436}
437/**
438 * Resolve to an instance of `ContextView` by the binding filter function
439 * for `@inject.view`
440 * @param ctx - Context object
441 * @param injection - Injection information
442 */
443function resolveAsContextView(ctx, injection) {
444 assertTargetType(injection, context_view_1.ContextView);
445 const bindingFilter = injection.bindingSelector;
446 const view = new context_view_1.ContextView(ctx, bindingFilter, injection.metadata.bindingComparator);
447 view.open();
448 return view;
449}
450/**
451 * Return a map of injection objects for properties
452 * @param target - The target class for static properties or
453 * prototype for instance properties.
454 */
455function describeInjectedProperties(target) {
456 var _a;
457 const metadata = (_a = metadata_1.MetadataInspector.getAllPropertyMetadata(INJECT_PROPERTIES_KEY, target)) !== null && _a !== void 0 ? _a : {};
458 return metadata;
459}
460exports.describeInjectedProperties = describeInjectedProperties;
461/**
462 * Inspect injections for a binding created with `toClass` or `toProvider`
463 * @param binding - Binding object
464 */
465function inspectInjections(binding) {
466 var _a;
467 const json = {};
468 const ctor = (_a = binding.valueConstructor) !== null && _a !== void 0 ? _a : binding.providerConstructor;
469 if (ctor == null)
470 return json;
471 const constructorInjections = describeInjectedArguments(ctor, '').map(inspectInjection);
472 if (constructorInjections.length) {
473 json.constructorArguments = constructorInjections;
474 }
475 const propertyInjections = describeInjectedProperties(ctor.prototype);
476 const properties = {};
477 for (const p in propertyInjections) {
478 properties[p] = inspectInjection(propertyInjections[p]);
479 }
480 if (Object.keys(properties).length) {
481 json.properties = properties;
482 }
483 return json;
484}
485exports.inspectInjections = inspectInjections;
486/**
487 * Inspect an injection
488 * @param injection - Injection information
489 */
490function inspectInjection(injection) {
491 var _a, _b;
492 const injectionInfo = resolution_session_1.ResolutionSession.describeInjection(injection);
493 const descriptor = {};
494 if (injectionInfo.targetName) {
495 descriptor.targetName = injectionInfo.targetName;
496 }
497 if ((0, binding_filter_1.isBindingAddress)(injectionInfo.bindingSelector)) {
498 // Binding key
499 descriptor.bindingKey = injectionInfo.bindingSelector.toString();
500 }
501 else if ((0, binding_filter_1.isBindingTagFilter)(injectionInfo.bindingSelector)) {
502 // Binding tag filter
503 descriptor.bindingTagPattern = JSON.parse(JSON.stringify(injectionInfo.bindingSelector.bindingTagPattern));
504 }
505 else {
506 // Binding filter function
507 descriptor.bindingFilter =
508 (_b = (_a = injectionInfo.bindingSelector) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : '<function>';
509 }
510 // Inspect metadata
511 if (injectionInfo.metadata) {
512 if (injectionInfo.metadata.decorator &&
513 injectionInfo.metadata.decorator !== '@inject') {
514 descriptor.decorator = injectionInfo.metadata.decorator;
515 }
516 if (injectionInfo.metadata.optional) {
517 descriptor.optional = injectionInfo.metadata.optional;
518 }
519 }
520 return descriptor;
521}
522/**
523 * Check if the given class has `@inject` or other decorations that map to
524 * `@inject`.
525 *
526 * @param cls - Class with possible `@inject` decorations
527 */
528function hasInjections(cls) {
529 return (metadata_1.MetadataInspector.getClassMetadata(INJECT_PARAMETERS_KEY, cls) != null ||
530 metadata_1.Reflector.getMetadata(INJECT_PARAMETERS_KEY.toString(), cls.prototype) !=
531 null ||
532 metadata_1.MetadataInspector.getAllPropertyMetadata(INJECT_PROPERTIES_KEY, cls.prototype) != null);
533}
534exports.hasInjections = hasInjections;
535//# sourceMappingURL=inject.js.map
\No newline at end of file