@loopback/context
Version:
Facilities to manage artifacts and their dependencies in your Node.js applications. The module exposes TypeScript/JavaScript APIs and decorators to register artifacts, declare dependencies, and resolve artifacts by keys. It also serves as an IoC container
536 lines • 21 kB
JavaScript
// Copyright IBM Corp. and LoopBack contributors 2017,2020. All Rights Reserved.
// Node module: @loopback/context
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
Object.defineProperty(exports, "__esModule", { value: true });
exports.hasInjections = exports.inspectInjections = exports.describeInjectedProperties = exports.inspectTargetType = exports.describeInjectedArguments = exports.assertTargetType = exports.Getter = exports.inject = void 0;
const metadata_1 = require("@loopback/metadata");
const binding_1 = require("./binding");
const binding_filter_1 = require("./binding-filter");
const context_view_1 = require("./context-view");
const resolution_session_1 = require("./resolution-session");
const INJECT_PARAMETERS_KEY = metadata_1.MetadataAccessor.create('inject:parameters');
const INJECT_PROPERTIES_KEY = metadata_1.MetadataAccessor.create('inject:properties');
// A key to cache described argument injections
const INJECT_METHODS_KEY = metadata_1.MetadataAccessor.create('inject:methods');
/**
* A decorator to annotate method arguments for automatic injection
* by LoopBack IoC container.
*
* @example
* Usage - Typescript:
*
* ```ts
* class InfoController {
* @inject('authentication.user') public userName: string;
*
* constructor(@inject('application.name') public appName: string) {
* }
* // ...
* }
* ```
*
* Usage - JavaScript:
*
* - TODO(bajtos)
*
* @param bindingSelector - What binding to use in order to resolve the value of the
* decorated constructor parameter or property.
* @param metadata - Optional metadata to help the injection
* @param resolve - Optional function to resolve the injection
*
*/
function inject(bindingSelector, metadata, resolve) {
if (typeof bindingSelector === 'function' && !resolve) {
resolve = resolveValuesByFilter;
}
const injectionMetadata = Object.assign({ decorator: '@inject' }, metadata);
if (injectionMetadata.bindingComparator && !resolve) {
throw new Error('Binding comparator is only allowed with a binding filter');
}
if (!bindingSelector && typeof resolve !== 'function') {
throw new Error('A non-empty binding selector or resolve function is required for @inject');
}
return function markParameterOrPropertyAsInjected(target, member, methodDescriptorOrParameterIndex) {
if (typeof methodDescriptorOrParameterIndex === 'number') {
// The decorator is applied to a method parameter
// Please note propertyKey is `undefined` for constructor
const paramDecorator = metadata_1.ParameterDecoratorFactory.createDecorator(INJECT_PARAMETERS_KEY, {
target,
member,
methodDescriptorOrParameterIndex,
bindingSelector,
metadata: injectionMetadata,
resolve,
},
// Do not deep clone the spec as only metadata is mutable and it's
// shallowly cloned
{ cloneInputSpec: false, decoratorName: injectionMetadata.decorator });
paramDecorator(target, member, methodDescriptorOrParameterIndex);
}
else if (member) {
// Property or method
if (target instanceof Function) {
throw new Error('@inject is not supported for a static property: ' +
metadata_1.DecoratorFactory.getTargetName(target, member));
}
if (methodDescriptorOrParameterIndex) {
// Method
throw new Error('@inject cannot be used on a method: ' +
metadata_1.DecoratorFactory.getTargetName(target, member, methodDescriptorOrParameterIndex));
}
const propDecorator = metadata_1.PropertyDecoratorFactory.createDecorator(INJECT_PROPERTIES_KEY, {
target,
member,
methodDescriptorOrParameterIndex,
bindingSelector,
metadata: injectionMetadata,
resolve,
},
// Do not deep clone the spec as only metadata is mutable and it's
// shallowly cloned
{ cloneInputSpec: false, decoratorName: injectionMetadata.decorator });
propDecorator(target, member);
}
else {
// It won't happen here as `@inject` is not compatible with ClassDecorator
/* istanbul ignore next */
throw new Error('@inject can only be used on a property or a method parameter');
}
};
}
exports.inject = inject;
var Getter;
(function (Getter) {
/**
* Convert a value into a Getter returning that value.
* @param value
*/
function fromValue(value) {
return () => Promise.resolve(value);
}
Getter.fromValue = fromValue;
})(Getter || (exports.Getter = Getter = {}));
(function (inject) {
/**
* Inject a function for getting the actual bound value.
*
* This is useful when implementing Actions, where
* the action is instantiated for Sequence constructor, but some
* of action's dependencies become bound only after other actions
* have been executed by the sequence.
*
* See also `Getter<T>`.
*
* @param bindingSelector - The binding key or filter we want to eventually get
* value(s) from.
* @param metadata - Optional metadata to help the injection
*/
inject.getter = function injectGetter(bindingSelector, metadata) {
metadata = Object.assign({ decorator: '@inject.getter' }, metadata);
return inject(bindingSelector, metadata, (0, binding_filter_1.isBindingAddress)(bindingSelector)
? resolveAsGetter
: resolveAsGetterByFilter);
};
/**
* Inject a function for setting (binding) the given key to a given
* value. (Only static/constant values are supported, it's not possible
* to bind a key to a class or a provider.)
*
* This is useful e.g. when implementing Actions that are contributing
* new Elements.
*
* See also `Setter<T>`.
*
* @param bindingKey - The key of the value we want to set.
* @param metadata - Optional metadata to help the injection
*/
inject.setter = function injectSetter(bindingKey, metadata) {
metadata = Object.assign({ decorator: '@inject.setter' }, metadata);
return inject(bindingKey, metadata, resolveAsSetter);
};
/**
* Inject the binding object for the given key. This is useful if a binding
* needs to be set up beyond just a constant value allowed by
* `@inject.setter`. The injected binding is found or created based on the
* `metadata.bindingCreation` option. See `BindingCreationPolicy` for more
* details.
*
* @example
*
* ```ts
* class MyAuthAction {
* @inject.binding('current-user', {
* bindingCreation: BindingCreationPolicy.ALWAYS_CREATE,
* })
* private userBinding: Binding<UserProfile>;
*
* async authenticate() {
* this.userBinding.toDynamicValue(() => {...});
* }
* }
* ```
*
* @param bindingKey - Binding key
* @param metadata - Metadata for the injection
*/
inject.binding = function injectBinding(bindingKey, metadata) {
metadata = Object.assign({ decorator: '@inject.binding' }, metadata);
return inject(bindingKey !== null && bindingKey !== void 0 ? bindingKey : '', metadata, resolveAsBinding);
};
/**
* Inject an array of values by a tag pattern string or regexp
*
* @example
* ```ts
* class AuthenticationManager {
* constructor(
* @inject.tag('authentication.strategy') public strategies: Strategy[],
* ) {}
* }
* ```
* @param bindingTag - Tag name, regex or object
* @param metadata - Optional metadata to help the injection
*/
inject.tag = function injectByTag(bindingTag, metadata) {
metadata = Object.assign({ decorator: '@inject.tag', tag: bindingTag }, metadata);
return inject((0, binding_filter_1.filterByTag)(bindingTag), metadata);
};
/**
* Inject matching bound values by the filter function
*
* @example
* ```ts
* class MyControllerWithView {
* @inject.view(filterByTag('foo'))
* view: ContextView<string[]>;
* }
* ```
* @param bindingFilter - A binding filter function
* @param metadata
*/
inject.view = function injectContextView(bindingFilter, metadata) {
metadata = Object.assign({ decorator: '@inject.view' }, metadata);
return inject(bindingFilter, metadata, resolveAsContextView);
};
/**
* Inject the context object.
*
* @example
* ```ts
* class MyProvider {
* constructor(@inject.context() private ctx: Context) {}
* }
* ```
*/
inject.context = function injectContext() {
return inject('', { decorator: '@inject.context' }, (ctx) => ctx);
};
})(inject || (exports.inject = inject = {}));
/**
* Assert the target type inspected from TypeScript for injection to be the
* expected type. If the types don't match, an error is thrown.
* @param injection - Injection information
* @param expectedType - Expected type
* @param expectedTypeName - Name of the expected type to be used in the error
* @returns The name of the target
*/
function assertTargetType(injection, expectedType, expectedTypeName) {
const targetName = resolution_session_1.ResolutionSession.describeInjection(injection).targetName;
const targetType = inspectTargetType(injection);
if (targetType && targetType !== expectedType) {
expectedTypeName = expectedTypeName !== null && expectedTypeName !== void 0 ? expectedTypeName : expectedType.name;
throw new Error(`The type of ${targetName} (${targetType.name}) is not ${expectedTypeName}`);
}
return targetName;
}
exports.assertTargetType = assertTargetType;
/**
* Resolver for `@inject.getter`
* @param ctx
* @param injection
* @param session
*/
function resolveAsGetter(ctx, injection, session) {
assertTargetType(injection, Function, 'Getter function');
const bindingSelector = injection.bindingSelector;
const options = {
// https://github.com/loopbackio/loopback-next/issues/9041
// We should start with a new session for `getter` resolution to avoid
// possible circular dependencies
session: undefined,
...injection.metadata,
};
return function getter() {
return ctx.get(bindingSelector, options);
};
}
/**
* Resolver for `@inject.setter`
* @param ctx
* @param injection
*/
function resolveAsSetter(ctx, injection) {
const targetName = assertTargetType(injection, Function, 'Setter function');
const bindingSelector = injection.bindingSelector;
if (!(0, binding_filter_1.isBindingAddress)(bindingSelector)) {
throw new Error(`@inject.setter (${targetName}) does not allow BindingFilter.`);
}
if (bindingSelector === '') {
throw new Error('Binding key is not set for @inject.setter');
}
// No resolution session should be propagated into the setter
return function setter(value) {
const binding = findOrCreateBindingForInjection(ctx, injection);
binding.to(value);
};
}
function resolveAsBinding(ctx, injection, session) {
const targetName = assertTargetType(injection, binding_1.Binding);
const bindingSelector = injection.bindingSelector;
if (!(0, binding_filter_1.isBindingAddress)(bindingSelector)) {
throw new Error(`@inject.binding (${targetName}) does not allow BindingFilter.`);
}
return findOrCreateBindingForInjection(ctx, injection, session);
}
function findOrCreateBindingForInjection(ctx, injection, session) {
if (injection.bindingSelector === '')
return session === null || session === void 0 ? void 0 : session.currentBinding;
const bindingCreation = injection.metadata &&
injection.metadata.bindingCreation;
const binding = ctx.findOrCreateBinding(injection.bindingSelector, bindingCreation);
return binding;
}
/**
* Check if constructor injection should be applied to the base class
* of the given target class
*
* @param targetClass - Target class
*/
function shouldSkipBaseConstructorInjection(targetClass) {
// FXIME(rfeng): We use the class definition to check certain patterns
const classDef = targetClass.toString();
return (
/*
* See https://github.com/loopbackio/loopback-next/issues/2946
* A class decorator can return a new constructor that mixes in
* additional properties/methods.
*
* @example
* ```ts
* class extends baseConstructor {
* // The constructor calls `super(...arguments)`
* constructor() {
* super(...arguments);
* }
* classProperty = 'a classProperty';
* classFunction() {
* return 'a classFunction';
* }
* };
* ```
*
* We check the following pattern:
* ```ts
* constructor() {
* super(...arguments);
* }
* ```
*/
!classDef.match(/\s+constructor\s*\(\s*\)\s*\{\s*super\(\.\.\.arguments\)/) &&
/*
* See https://github.com/loopbackio/loopback-next/issues/1565
*
* @example
* ```ts
* class BaseClass {
* constructor(@inject('foo') protected foo: string) {}
* // ...
* }
*
* class SubClass extends BaseClass {
* // No explicit constructor is present
*
* @inject('bar')
* private bar: number;
* // ...
* };
*
*/
classDef.match(/\s+constructor\s*\([^\)]*\)\s+\{/m));
}
/**
* Return an array of injection objects for parameters
* @param target - The target class for constructor or static methods,
* or the prototype for instance methods
* @param method - Method name, undefined for constructor
*/
function describeInjectedArguments(target, method) {
var _a, _b;
method = method !== null && method !== void 0 ? method : '';
// Try to read from cache
const cache = (_a = metadata_1.MetadataInspector.getAllMethodMetadata(INJECT_METHODS_KEY, target, {
ownMetadataOnly: true,
})) !== null && _a !== void 0 ? _a : {};
let meta = cache[method];
if (meta)
return meta;
// Build the description
const options = {};
if (method === '') {
if (shouldSkipBaseConstructorInjection(target)) {
options.ownMetadataOnly = true;
}
}
else if (Object.prototype.hasOwnProperty.call(target, method)) {
// The method exists in the target, no injections on the super method
// should be honored
options.ownMetadataOnly = true;
}
meta =
(_b = metadata_1.MetadataInspector.getAllParameterMetadata(INJECT_PARAMETERS_KEY, target, method, options)) !== null && _b !== void 0 ? _b : [];
// Cache the result
cache[method] = meta;
metadata_1.MetadataInspector.defineMetadata(INJECT_METHODS_KEY, cache, target);
return meta;
}
exports.describeInjectedArguments = describeInjectedArguments;
/**
* Inspect the target type for the injection to find out the corresponding
* JavaScript type
* @param injection - Injection information
*/
function inspectTargetType(injection) {
var _a;
if (typeof injection.methodDescriptorOrParameterIndex === 'number') {
const designType = metadata_1.MetadataInspector.getDesignTypeForMethod(injection.target, injection.member);
return (_a = designType === null || designType === void 0 ? void 0 : designType.parameterTypes) === null || _a === void 0 ? void 0 : _a[injection.methodDescriptorOrParameterIndex];
}
return metadata_1.MetadataInspector.getDesignTypeForProperty(injection.target, injection.member);
}
exports.inspectTargetType = inspectTargetType;
/**
* Resolve an array of bound values matching the filter function for `@inject`.
* @param ctx - Context object
* @param injection - Injection information
* @param session - Resolution session
*/
function resolveValuesByFilter(ctx, injection, session) {
assertTargetType(injection, Array);
const bindingFilter = injection.bindingSelector;
const view = new context_view_1.ContextView(ctx, bindingFilter, injection.metadata.bindingComparator);
return view.resolve(session);
}
/**
* Resolve to a getter function that returns an array of bound values matching
* the filter function for `@inject.getter`.
*
* @param ctx - Context object
* @param injection - Injection information
* @param session - Resolution session
*/
function resolveAsGetterByFilter(ctx, injection, session) {
assertTargetType(injection, Function, 'Getter function');
const bindingFilter = injection.bindingSelector;
return (0, context_view_1.createViewGetter)(ctx, bindingFilter, injection.metadata.bindingComparator, session);
}
/**
* Resolve to an instance of `ContextView` by the binding filter function
* for `@inject.view`
* @param ctx - Context object
* @param injection - Injection information
*/
function resolveAsContextView(ctx, injection) {
assertTargetType(injection, context_view_1.ContextView);
const bindingFilter = injection.bindingSelector;
const view = new context_view_1.ContextView(ctx, bindingFilter, injection.metadata.bindingComparator);
view.open();
return view;
}
/**
* Return a map of injection objects for properties
* @param target - The target class for static properties or
* prototype for instance properties.
*/
function describeInjectedProperties(target) {
var _a;
const metadata = (_a = metadata_1.MetadataInspector.getAllPropertyMetadata(INJECT_PROPERTIES_KEY, target)) !== null && _a !== void 0 ? _a : {};
return metadata;
}
exports.describeInjectedProperties = describeInjectedProperties;
/**
* Inspect injections for a binding created with `toClass` or `toProvider`
* @param binding - Binding object
*/
function inspectInjections(binding) {
var _a;
const json = {};
const ctor = (_a = binding.valueConstructor) !== null && _a !== void 0 ? _a : binding.providerConstructor;
if (ctor == null)
return json;
const constructorInjections = describeInjectedArguments(ctor, '').map(inspectInjection);
if (constructorInjections.length) {
json.constructorArguments = constructorInjections;
}
const propertyInjections = describeInjectedProperties(ctor.prototype);
const properties = {};
for (const p in propertyInjections) {
properties[p] = inspectInjection(propertyInjections[p]);
}
if (Object.keys(properties).length) {
json.properties = properties;
}
return json;
}
exports.inspectInjections = inspectInjections;
/**
* Inspect an injection
* @param injection - Injection information
*/
function inspectInjection(injection) {
var _a, _b;
const injectionInfo = resolution_session_1.ResolutionSession.describeInjection(injection);
const descriptor = {};
if (injectionInfo.targetName) {
descriptor.targetName = injectionInfo.targetName;
}
if ((0, binding_filter_1.isBindingAddress)(injectionInfo.bindingSelector)) {
// Binding key
descriptor.bindingKey = injectionInfo.bindingSelector.toString();
}
else if ((0, binding_filter_1.isBindingTagFilter)(injectionInfo.bindingSelector)) {
// Binding tag filter
descriptor.bindingTagPattern = JSON.parse(JSON.stringify(injectionInfo.bindingSelector.bindingTagPattern));
}
else {
// Binding filter function
descriptor.bindingFilter =
(_b = (_a = injectionInfo.bindingSelector) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : '<function>';
}
// Inspect metadata
if (injectionInfo.metadata) {
if (injectionInfo.metadata.decorator &&
injectionInfo.metadata.decorator !== '@inject') {
descriptor.decorator = injectionInfo.metadata.decorator;
}
if (injectionInfo.metadata.optional) {
descriptor.optional = injectionInfo.metadata.optional;
}
}
return descriptor;
}
/**
* Check if the given class has `@inject` or other decorations that map to
* `@inject`.
*
* @param cls - Class with possible `@inject` decorations
*/
function hasInjections(cls) {
return (metadata_1.MetadataInspector.getClassMetadata(INJECT_PARAMETERS_KEY, cls) != null ||
metadata_1.Reflector.getMetadata(INJECT_PARAMETERS_KEY.toString(), cls.prototype) !=
null ||
metadata_1.MetadataInspector.getAllPropertyMetadata(INJECT_PROPERTIES_KEY, cls.prototype) != null);
}
exports.hasInjections = hasInjections;
//# sourceMappingURL=inject.js.map
;