UNPKG

23.2 kBPlain TextView Raw
1// Copyright IBM Corp. and LoopBack contributors 2017,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 {
7 DecoratorFactory,
8 InspectionOptions,
9 MetadataAccessor,
10 MetadataInspector,
11 MetadataMap,
12 ParameterDecoratorFactory,
13 PropertyDecoratorFactory,
14 Reflector,
15} from '@loopback/metadata';
16import {Binding, BindingTag} from './binding';
17import {
18 BindingFilter,
19 BindingSelector,
20 filterByTag,
21 isBindingAddress,
22 isBindingTagFilter,
23} from './binding-filter';
24import {BindingAddress, BindingKey} from './binding-key';
25import {BindingComparator} from './binding-sorter';
26import {BindingCreationPolicy, Context} from './context';
27import {ContextView, createViewGetter} from './context-view';
28import {JSONObject} from './json-types';
29import {ResolutionOptions, ResolutionSession} from './resolution-session';
30import {BoundValue, Constructor, ValueOrPromise} from './value-promise';
31
32const INJECT_PARAMETERS_KEY = MetadataAccessor.create<
33 Injection,
34 ParameterDecorator
35>('inject:parameters');
36
37const INJECT_PROPERTIES_KEY = MetadataAccessor.create<
38 Injection,
39 PropertyDecorator
40>('inject:properties');
41
42// A key to cache described argument injections
43const INJECT_METHODS_KEY = MetadataAccessor.create<Injection, MethodDecorator>(
44 'inject:methods',
45);
46
47// TODO(rfeng): We may want to align it with `ValueFactory` interface that takes
48// an argument of `ResolutionContext`.
49/**
50 * A function to provide resolution of injected values.
51 *
52 * @example
53 * ```ts
54 * const resolver: ResolverFunction = (ctx, injection, session) {
55 * return session.currentBinding?.key;
56 * }
57 * ```
58 */
59export interface ResolverFunction {
60 (
61 ctx: Context,
62 injection: Readonly<Injection>,
63 session: ResolutionSession,
64 ): ValueOrPromise<BoundValue>;
65}
66
67/**
68 * An object to provide metadata for `@inject`
69 */
70export interface InjectionMetadata extends Omit<ResolutionOptions, 'session'> {
71 /**
72 * Name of the decorator function, such as `@inject` or `@inject.setter`.
73 * It's usually set by the decorator implementation.
74 */
75 decorator?: string;
76 /**
77 * Optional comparator for matched bindings
78 */
79 bindingComparator?: BindingComparator;
80 /**
81 * Other attributes
82 */
83 [attribute: string]: BoundValue;
84}
85
86/**
87 * Descriptor for an injection point
88 */
89export interface Injection<ValueType = BoundValue> {
90 target: Object;
91 member?: string;
92 methodDescriptorOrParameterIndex?:
93 | TypedPropertyDescriptor<ValueType>
94 | number;
95
96 bindingSelector: BindingSelector<ValueType>; // Binding selector
97 metadata: InjectionMetadata; // Related metadata
98 resolve?: ResolverFunction; // A custom resolve function
99}
100
101/**
102 * A decorator to annotate method arguments for automatic injection
103 * by LoopBack IoC container.
104 *
105 * @example
106 * Usage - Typescript:
107 *
108 * ```ts
109 * class InfoController {
110 * @inject('authentication.user') public userName: string;
111 *
112 * constructor(@inject('application.name') public appName: string) {
113 * }
114 * // ...
115 * }
116 * ```
117 *
118 * Usage - JavaScript:
119 *
120 * - TODO(bajtos)
121 *
122 * @param bindingSelector - What binding to use in order to resolve the value of the
123 * decorated constructor parameter or property.
124 * @param metadata - Optional metadata to help the injection
125 * @param resolve - Optional function to resolve the injection
126 *
127 */
128export function inject(
129 bindingSelector: BindingSelector,
130 metadata?: InjectionMetadata,
131 resolve?: ResolverFunction,
132) {
133 if (typeof bindingSelector === 'function' && !resolve) {
134 resolve = resolveValuesByFilter;
135 }
136 const injectionMetadata = Object.assign({decorator: '@inject'}, metadata);
137 if (injectionMetadata.bindingComparator && !resolve) {
138 throw new Error('Binding comparator is only allowed with a binding filter');
139 }
140 if (!bindingSelector && typeof resolve !== 'function') {
141 throw new Error(
142 'A non-empty binding selector or resolve function is required for @inject',
143 );
144 }
145 return function markParameterOrPropertyAsInjected(
146 target: Object,
147 member: string | undefined,
148 methodDescriptorOrParameterIndex?:
149 | TypedPropertyDescriptor<BoundValue>
150 | number,
151 ) {
152 if (typeof methodDescriptorOrParameterIndex === 'number') {
153 // The decorator is applied to a method parameter
154 // Please note propertyKey is `undefined` for constructor
155 const paramDecorator: ParameterDecorator =
156 ParameterDecoratorFactory.createDecorator<Injection>(
157 INJECT_PARAMETERS_KEY,
158 {
159 target,
160 member,
161 methodDescriptorOrParameterIndex,
162 bindingSelector,
163 metadata: injectionMetadata,
164 resolve,
165 },
166 // Do not deep clone the spec as only metadata is mutable and it's
167 // shallowly cloned
168 {cloneInputSpec: false, decoratorName: injectionMetadata.decorator},
169 );
170 paramDecorator(target, member!, methodDescriptorOrParameterIndex);
171 } else if (member) {
172 // Property or method
173 if (target instanceof Function) {
174 throw new Error(
175 '@inject is not supported for a static property: ' +
176 DecoratorFactory.getTargetName(target, member),
177 );
178 }
179 if (methodDescriptorOrParameterIndex) {
180 // Method
181 throw new Error(
182 '@inject cannot be used on a method: ' +
183 DecoratorFactory.getTargetName(
184 target,
185 member,
186 methodDescriptorOrParameterIndex,
187 ),
188 );
189 }
190 const propDecorator: PropertyDecorator =
191 PropertyDecoratorFactory.createDecorator<Injection>(
192 INJECT_PROPERTIES_KEY,
193 {
194 target,
195 member,
196 methodDescriptorOrParameterIndex,
197 bindingSelector,
198 metadata: injectionMetadata,
199 resolve,
200 },
201 // Do not deep clone the spec as only metadata is mutable and it's
202 // shallowly cloned
203 {cloneInputSpec: false, decoratorName: injectionMetadata.decorator},
204 );
205 propDecorator(target, member!);
206 } else {
207 // It won't happen here as `@inject` is not compatible with ClassDecorator
208 /* istanbul ignore next */
209 throw new Error(
210 '@inject can only be used on a property or a method parameter',
211 );
212 }
213 };
214}
215
216/**
217 * The function injected by `@inject.getter(bindingSelector)`. It can be used
218 * to fetch bound value(s) from the underlying binding(s). The return value will
219 * be an array if the `bindingSelector` is a `BindingFilter` function.
220 */
221export type Getter<T> = () => Promise<T>;
222
223export namespace Getter {
224 /**
225 * Convert a value into a Getter returning that value.
226 * @param value
227 */
228 export function fromValue<T>(value: T): Getter<T> {
229 return () => Promise.resolve(value);
230 }
231}
232
233/**
234 * The function injected by `@inject.setter(bindingKey)`. It sets the underlying
235 * binding to a constant value using `binding.to(value)`.
236 *
237 * @example
238 *
239 * ```ts
240 * setterFn('my-value');
241 * ```
242 * @param value - The value for the underlying binding
243 */
244export type Setter<T> = (value: T) => void;
245
246/**
247 * Metadata for `@inject.binding`
248 */
249export interface InjectBindingMetadata extends InjectionMetadata {
250 /**
251 * Controls how the underlying binding is resolved/created
252 */
253 bindingCreation?: BindingCreationPolicy;
254}
255
256export namespace inject {
257 /**
258 * Inject a function for getting the actual bound value.
259 *
260 * This is useful when implementing Actions, where
261 * the action is instantiated for Sequence constructor, but some
262 * of action's dependencies become bound only after other actions
263 * have been executed by the sequence.
264 *
265 * See also `Getter<T>`.
266 *
267 * @param bindingSelector - The binding key or filter we want to eventually get
268 * value(s) from.
269 * @param metadata - Optional metadata to help the injection
270 */
271 export const getter = function injectGetter(
272 bindingSelector: BindingSelector<unknown>,
273 metadata?: InjectionMetadata,
274 ) {
275 metadata = Object.assign({decorator: '@inject.getter'}, metadata);
276 return inject(
277 bindingSelector,
278 metadata,
279 isBindingAddress(bindingSelector)
280 ? resolveAsGetter
281 : resolveAsGetterByFilter,
282 );
283 };
284
285 /**
286 * Inject a function for setting (binding) the given key to a given
287 * value. (Only static/constant values are supported, it's not possible
288 * to bind a key to a class or a provider.)
289 *
290 * This is useful e.g. when implementing Actions that are contributing
291 * new Elements.
292 *
293 * See also `Setter<T>`.
294 *
295 * @param bindingKey - The key of the value we want to set.
296 * @param metadata - Optional metadata to help the injection
297 */
298 export const setter = function injectSetter(
299 bindingKey: BindingAddress,
300 metadata?: InjectBindingMetadata,
301 ) {
302 metadata = Object.assign({decorator: '@inject.setter'}, metadata);
303 return inject(bindingKey, metadata, resolveAsSetter);
304 };
305
306 /**
307 * Inject the binding object for the given key. This is useful if a binding
308 * needs to be set up beyond just a constant value allowed by
309 * `@inject.setter`. The injected binding is found or created based on the
310 * `metadata.bindingCreation` option. See `BindingCreationPolicy` for more
311 * details.
312 *
313 * @example
314 *
315 * ```ts
316 * class MyAuthAction {
317 * @inject.binding('current-user', {
318 * bindingCreation: BindingCreationPolicy.ALWAYS_CREATE,
319 * })
320 * private userBinding: Binding<UserProfile>;
321 *
322 * async authenticate() {
323 * this.userBinding.toDynamicValue(() => {...});
324 * }
325 * }
326 * ```
327 *
328 * @param bindingKey - Binding key
329 * @param metadata - Metadata for the injection
330 */
331 export const binding = function injectBinding(
332 bindingKey?: string | BindingKey<unknown>,
333 metadata?: InjectBindingMetadata,
334 ) {
335 metadata = Object.assign({decorator: '@inject.binding'}, metadata);
336 return inject(bindingKey ?? '', metadata, resolveAsBinding);
337 };
338
339 /**
340 * Inject an array of values by a tag pattern string or regexp
341 *
342 * @example
343 * ```ts
344 * class AuthenticationManager {
345 * constructor(
346 * @inject.tag('authentication.strategy') public strategies: Strategy[],
347 * ) {}
348 * }
349 * ```
350 * @param bindingTag - Tag name, regex or object
351 * @param metadata - Optional metadata to help the injection
352 */
353 export const tag = function injectByTag(
354 bindingTag: BindingTag | RegExp,
355 metadata?: InjectionMetadata,
356 ) {
357 metadata = Object.assign(
358 {decorator: '@inject.tag', tag: bindingTag},
359 metadata,
360 );
361 return inject(filterByTag(bindingTag), metadata);
362 };
363
364 /**
365 * Inject matching bound values by the filter function
366 *
367 * @example
368 * ```ts
369 * class MyControllerWithView {
370 * @inject.view(filterByTag('foo'))
371 * view: ContextView<string[]>;
372 * }
373 * ```
374 * @param bindingFilter - A binding filter function
375 * @param metadata
376 */
377 export const view = function injectContextView(
378 bindingFilter: BindingFilter,
379 metadata?: InjectionMetadata,
380 ) {
381 metadata = Object.assign({decorator: '@inject.view'}, metadata);
382 return inject(bindingFilter, metadata, resolveAsContextView);
383 };
384
385 /**
386 * Inject the context object.
387 *
388 * @example
389 * ```ts
390 * class MyProvider {
391 * constructor(@inject.context() private ctx: Context) {}
392 * }
393 * ```
394 */
395 export const context = function injectContext() {
396 return inject('', {decorator: '@inject.context'}, (ctx: Context) => ctx);
397 };
398}
399
400/**
401 * Assert the target type inspected from TypeScript for injection to be the
402 * expected type. If the types don't match, an error is thrown.
403 * @param injection - Injection information
404 * @param expectedType - Expected type
405 * @param expectedTypeName - Name of the expected type to be used in the error
406 * @returns The name of the target
407 */
408export function assertTargetType(
409 injection: Readonly<Injection>,
410 expectedType: Function,
411 expectedTypeName?: string,
412) {
413 const targetName = ResolutionSession.describeInjection(injection).targetName;
414 const targetType = inspectTargetType(injection);
415 if (targetType && targetType !== expectedType) {
416 expectedTypeName = expectedTypeName ?? expectedType.name;
417 throw new Error(
418 `The type of ${targetName} (${targetType.name}) is not ${expectedTypeName}`,
419 );
420 }
421 return targetName;
422}
423
424/**
425 * Resolver for `@inject.getter`
426 * @param ctx
427 * @param injection
428 * @param session
429 */
430function resolveAsGetter(
431 ctx: Context,
432 injection: Readonly<Injection>,
433 session: ResolutionSession,
434) {
435 assertTargetType(injection, Function, 'Getter function');
436 const bindingSelector = injection.bindingSelector as BindingAddress;
437 const options: ResolutionOptions = {
438 // https://github.com/loopbackio/loopback-next/issues/9041
439 // We should start with a new session for `getter` resolution to avoid
440 // possible circular dependencies
441 session: undefined,
442 ...injection.metadata,
443 };
444 return function getter() {
445 return ctx.get(bindingSelector, options);
446 };
447}
448
449/**
450 * Resolver for `@inject.setter`
451 * @param ctx
452 * @param injection
453 */
454function resolveAsSetter(ctx: Context, injection: Injection) {
455 const targetName = assertTargetType(injection, Function, 'Setter function');
456 const bindingSelector = injection.bindingSelector;
457 if (!isBindingAddress(bindingSelector)) {
458 throw new Error(
459 `@inject.setter (${targetName}) does not allow BindingFilter.`,
460 );
461 }
462 if (bindingSelector === '') {
463 throw new Error('Binding key is not set for @inject.setter');
464 }
465 // No resolution session should be propagated into the setter
466 return function setter(value: unknown) {
467 const binding = findOrCreateBindingForInjection(ctx, injection);
468 binding!.to(value);
469 };
470}
471
472function resolveAsBinding(
473 ctx: Context,
474 injection: Injection,
475 session: ResolutionSession,
476) {
477 const targetName = assertTargetType(injection, Binding);
478 const bindingSelector = injection.bindingSelector;
479 if (!isBindingAddress(bindingSelector)) {
480 throw new Error(
481 `@inject.binding (${targetName}) does not allow BindingFilter.`,
482 );
483 }
484 return findOrCreateBindingForInjection(ctx, injection, session);
485}
486
487function findOrCreateBindingForInjection(
488 ctx: Context,
489 injection: Injection<unknown>,
490 session?: ResolutionSession,
491) {
492 if (injection.bindingSelector === '') return session?.currentBinding;
493 const bindingCreation =
494 injection.metadata &&
495 (injection.metadata as InjectBindingMetadata).bindingCreation;
496 const binding: Binding<unknown> = ctx.findOrCreateBinding(
497 injection.bindingSelector as BindingAddress,
498 bindingCreation,
499 );
500 return binding;
501}
502
503/**
504 * Check if constructor injection should be applied to the base class
505 * of the given target class
506 *
507 * @param targetClass - Target class
508 */
509function shouldSkipBaseConstructorInjection(targetClass: Object) {
510 // FXIME(rfeng): We use the class definition to check certain patterns
511 const classDef = targetClass.toString();
512 return (
513 /*
514 * See https://github.com/loopbackio/loopback-next/issues/2946
515 * A class decorator can return a new constructor that mixes in
516 * additional properties/methods.
517 *
518 * @example
519 * ```ts
520 * class extends baseConstructor {
521 * // The constructor calls `super(...arguments)`
522 * constructor() {
523 * super(...arguments);
524 * }
525 * classProperty = 'a classProperty';
526 * classFunction() {
527 * return 'a classFunction';
528 * }
529 * };
530 * ```
531 *
532 * We check the following pattern:
533 * ```ts
534 * constructor() {
535 * super(...arguments);
536 * }
537 * ```
538 */
539 !classDef.match(
540 /\s+constructor\s*\(\s*\)\s*\{\s*super\(\.\.\.arguments\)/,
541 ) &&
542 /*
543 * See https://github.com/loopbackio/loopback-next/issues/1565
544 *
545 * @example
546 * ```ts
547 * class BaseClass {
548 * constructor(@inject('foo') protected foo: string) {}
549 * // ...
550 * }
551 *
552 * class SubClass extends BaseClass {
553 * // No explicit constructor is present
554 *
555 * @inject('bar')
556 * private bar: number;
557 * // ...
558 * };
559 *
560 */
561 classDef.match(/\s+constructor\s*\([^\)]*\)\s+\{/m)
562 );
563}
564
565/**
566 * Return an array of injection objects for parameters
567 * @param target - The target class for constructor or static methods,
568 * or the prototype for instance methods
569 * @param method - Method name, undefined for constructor
570 */
571export function describeInjectedArguments(
572 target: Object,
573 method?: string,
574): Readonly<Injection>[] {
575 method = method ?? '';
576
577 // Try to read from cache
578 const cache =
579 MetadataInspector.getAllMethodMetadata<Readonly<Injection>[]>(
580 INJECT_METHODS_KEY,
581 target,
582 {
583 ownMetadataOnly: true,
584 },
585 ) ?? {};
586 let meta: Readonly<Injection>[] = cache[method];
587 if (meta) return meta;
588
589 // Build the description
590 const options: InspectionOptions = {};
591 if (method === '') {
592 if (shouldSkipBaseConstructorInjection(target)) {
593 options.ownMetadataOnly = true;
594 }
595 } else if (Object.prototype.hasOwnProperty.call(target, method)) {
596 // The method exists in the target, no injections on the super method
597 // should be honored
598 options.ownMetadataOnly = true;
599 }
600 meta =
601 MetadataInspector.getAllParameterMetadata<Readonly<Injection>>(
602 INJECT_PARAMETERS_KEY,
603 target,
604 method,
605 options,
606 ) ?? [];
607
608 // Cache the result
609 cache[method] = meta;
610 MetadataInspector.defineMetadata<MetadataMap<Readonly<Injection>[]>>(
611 INJECT_METHODS_KEY,
612 cache,
613 target,
614 );
615 return meta;
616}
617
618/**
619 * Inspect the target type for the injection to find out the corresponding
620 * JavaScript type
621 * @param injection - Injection information
622 */
623export function inspectTargetType(injection: Readonly<Injection>) {
624 if (typeof injection.methodDescriptorOrParameterIndex === 'number') {
625 const designType = MetadataInspector.getDesignTypeForMethod(
626 injection.target,
627 injection.member!,
628 );
629 return designType?.parameterTypes?.[
630 injection.methodDescriptorOrParameterIndex as number
631 ];
632 }
633 return MetadataInspector.getDesignTypeForProperty(
634 injection.target,
635 injection.member!,
636 );
637}
638
639/**
640 * Resolve an array of bound values matching the filter function for `@inject`.
641 * @param ctx - Context object
642 * @param injection - Injection information
643 * @param session - Resolution session
644 */
645function resolveValuesByFilter(
646 ctx: Context,
647 injection: Readonly<Injection>,
648 session: ResolutionSession,
649) {
650 assertTargetType(injection, Array);
651 const bindingFilter = injection.bindingSelector as BindingFilter;
652 const view = new ContextView(
653 ctx,
654 bindingFilter,
655 injection.metadata.bindingComparator,
656 );
657 return view.resolve(session);
658}
659
660/**
661 * Resolve to a getter function that returns an array of bound values matching
662 * the filter function for `@inject.getter`.
663 *
664 * @param ctx - Context object
665 * @param injection - Injection information
666 * @param session - Resolution session
667 */
668function resolveAsGetterByFilter(
669 ctx: Context,
670 injection: Readonly<Injection>,
671 session: ResolutionSession,
672) {
673 assertTargetType(injection, Function, 'Getter function');
674 const bindingFilter = injection.bindingSelector as BindingFilter;
675 return createViewGetter(
676 ctx,
677 bindingFilter,
678 injection.metadata.bindingComparator,
679 session,
680 );
681}
682
683/**
684 * Resolve to an instance of `ContextView` by the binding filter function
685 * for `@inject.view`
686 * @param ctx - Context object
687 * @param injection - Injection information
688 */
689function resolveAsContextView(ctx: Context, injection: Readonly<Injection>) {
690 assertTargetType(injection, ContextView);
691
692 const bindingFilter = injection.bindingSelector as BindingFilter;
693 const view = new ContextView(
694 ctx,
695 bindingFilter,
696 injection.metadata.bindingComparator,
697 );
698 view.open();
699 return view;
700}
701
702/**
703 * Return a map of injection objects for properties
704 * @param target - The target class for static properties or
705 * prototype for instance properties.
706 */
707export function describeInjectedProperties(
708 target: Object,
709): MetadataMap<Readonly<Injection>> {
710 const metadata =
711 MetadataInspector.getAllPropertyMetadata<Readonly<Injection>>(
712 INJECT_PROPERTIES_KEY,
713 target,
714 ) ?? {};
715 return metadata;
716}
717
718/**
719 * Inspect injections for a binding created with `toClass` or `toProvider`
720 * @param binding - Binding object
721 */
722export function inspectInjections(binding: Readonly<Binding<unknown>>) {
723 const json: JSONObject = {};
724 const ctor = binding.valueConstructor ?? binding.providerConstructor;
725 if (ctor == null) return json;
726 const constructorInjections = describeInjectedArguments(ctor, '').map(
727 inspectInjection,
728 );
729 if (constructorInjections.length) {
730 json.constructorArguments = constructorInjections;
731 }
732 const propertyInjections = describeInjectedProperties(ctor.prototype);
733 const properties: JSONObject = {};
734 for (const p in propertyInjections) {
735 properties[p] = inspectInjection(propertyInjections[p]);
736 }
737 if (Object.keys(properties).length) {
738 json.properties = properties;
739 }
740 return json;
741}
742
743/**
744 * Inspect an injection
745 * @param injection - Injection information
746 */
747function inspectInjection(injection: Readonly<Injection<unknown>>) {
748 const injectionInfo = ResolutionSession.describeInjection(injection);
749 const descriptor: JSONObject = {};
750 if (injectionInfo.targetName) {
751 descriptor.targetName = injectionInfo.targetName;
752 }
753 if (isBindingAddress(injectionInfo.bindingSelector)) {
754 // Binding key
755 descriptor.bindingKey = injectionInfo.bindingSelector.toString();
756 } else if (isBindingTagFilter(injectionInfo.bindingSelector)) {
757 // Binding tag filter
758 descriptor.bindingTagPattern = JSON.parse(
759 JSON.stringify(injectionInfo.bindingSelector.bindingTagPattern),
760 );
761 } else {
762 // Binding filter function
763 descriptor.bindingFilter =
764 injectionInfo.bindingSelector?.name ?? '<function>';
765 }
766 // Inspect metadata
767 if (injectionInfo.metadata) {
768 if (
769 injectionInfo.metadata.decorator &&
770 injectionInfo.metadata.decorator !== '@inject'
771 ) {
772 descriptor.decorator = injectionInfo.metadata.decorator;
773 }
774 if (injectionInfo.metadata.optional) {
775 descriptor.optional = injectionInfo.metadata.optional;
776 }
777 }
778 return descriptor;
779}
780
781/**
782 * Check if the given class has `@inject` or other decorations that map to
783 * `@inject`.
784 *
785 * @param cls - Class with possible `@inject` decorations
786 */
787export function hasInjections(cls: Constructor<unknown>): boolean {
788 return (
789 MetadataInspector.getClassMetadata(INJECT_PARAMETERS_KEY, cls) != null ||
790 Reflector.getMetadata(INJECT_PARAMETERS_KEY.toString(), cls.prototype) !=
791 null ||
792 MetadataInspector.getAllPropertyMetadata(
793 INJECT_PROPERTIES_KEY,
794 cls.prototype,
795 ) != null
796 );
797}