@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
203 lines (201 loc) • 9.47 kB
JavaScript
// Copyright IBM Corp. and LoopBack contributors 2017,2019. 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.resolveInjectedProperties = exports.resolveInjectedArguments = exports.instantiateClass = void 0;
const tslib_1 = require("tslib");
const metadata_1 = require("@loopback/metadata");
const assert_1 = tslib_1.__importDefault(require("assert"));
const debug_1 = tslib_1.__importDefault(require("debug"));
const binding_filter_1 = require("./binding-filter");
const inject_1 = require("./inject");
const resolution_session_1 = require("./resolution-session");
const value_promise_1 = require("./value-promise");
const debug = (0, debug_1.default)('loopback:context:resolver');
const getTargetName = metadata_1.DecoratorFactory.getTargetName;
/**
* Create an instance of a class which constructor has arguments
* decorated with `@inject`.
*
* The function returns a class when all dependencies were
* resolved synchronously, or a Promise otherwise.
*
* @param ctor - The class constructor to call.
* @param ctx - The context containing values for `@inject` resolution
* @param session - Optional session for binding and dependency resolution
* @param nonInjectedArgs - Optional array of args for non-injected parameters
*/
function instantiateClass(ctor, ctx, session,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nonInjectedArgs) {
/* istanbul ignore if */
if (debug.enabled) {
debug('Instantiating %s', getTargetName(ctor));
if (nonInjectedArgs === null || nonInjectedArgs === void 0 ? void 0 : nonInjectedArgs.length) {
debug('Non-injected arguments:', nonInjectedArgs);
}
}
const argsOrPromise = resolveInjectedArguments(ctor, '', ctx, session, nonInjectedArgs);
const propertiesOrPromise = resolveInjectedProperties(ctor, ctx, session);
const inst = (0, value_promise_1.transformValueOrPromise)(argsOrPromise, args => {
/* istanbul ignore if */
if (debug.enabled) {
debug('Injected arguments for %s():', ctor.name, args);
}
return new ctor(...args);
});
return (0, value_promise_1.transformValueOrPromise)(propertiesOrPromise, props => {
/* istanbul ignore if */
if (debug.enabled) {
debug('Injected properties for %s:', ctor.name, props);
}
return (0, value_promise_1.transformValueOrPromise)(inst, obj => Object.assign(obj, props));
});
}
exports.instantiateClass = instantiateClass;
/**
* If the scope of current binding is `SINGLETON`, reset the context
* to be the one that owns the current binding to make sure a singleton
* does not have dependencies injected from child contexts unless the
* injection is for method (excluding constructor) parameters.
*/
function resolveContext(ctx, injection, session) {
const currentBinding = session === null || session === void 0 ? void 0 : session.currentBinding;
if (currentBinding == null) {
// No current binding
return ctx;
}
const isConstructorOrPropertyInjection =
// constructor injection
!injection.member ||
// property injection
typeof injection.methodDescriptorOrParameterIndex !== 'number';
if (isConstructorOrPropertyInjection) {
// Set context to the resolution context of the current binding for
// constructor or property injections against a singleton
ctx = ctx.getResolutionContext(currentBinding);
}
return ctx;
}
/**
* Resolve the value or promise for a given injection
* @param ctx - Context
* @param injection - Descriptor of the injection
* @param session - Optional session for binding and dependency resolution
*/
function resolve(ctx, injection, session) {
/* istanbul ignore if */
if (debug.enabled) {
debug('Resolving an injection:', resolution_session_1.ResolutionSession.describeInjection(injection));
}
ctx = resolveContext(ctx, injection, session);
const resolved = resolution_session_1.ResolutionSession.runWithInjection(s => {
if (injection.resolve) {
// A custom resolve function is provided
return injection.resolve(ctx, injection, s);
}
else {
// Default to resolve the value from the context by binding key
(0, assert_1.default)((0, binding_filter_1.isBindingAddress)(injection.bindingSelector), 'The binding selector must be an address (string or BindingKey)');
const key = injection.bindingSelector;
const options = {
session: s,
...injection.metadata,
};
return ctx.getValueOrPromise(key, options);
}
}, injection, session);
return resolved;
}
/**
* Given a function with arguments decorated with `@inject`,
* return the list of arguments resolved using the values
* bound in `ctx`.
* The function returns an argument array when all dependencies were
* resolved synchronously, or a Promise otherwise.
*
* @param target - The class for constructor injection or prototype for method
* injection
* @param method - The method name. If set to '', the constructor will
* be used.
* @param ctx - The context containing values for `@inject` resolution
* @param session - Optional session for binding and dependency resolution
* @param nonInjectedArgs - Optional array of args for non-injected parameters
*/
function resolveInjectedArguments(target, method, ctx, session,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nonInjectedArgs) {
/* istanbul ignore if */
if (debug.enabled) {
debug('Resolving injected arguments for %s', getTargetName(target, method));
}
const targetWithMethods = target;
if (method) {
(0, assert_1.default)(typeof targetWithMethods[method] === 'function', `Method ${method} not found`);
}
// NOTE: the array may be sparse, i.e.
// Object.keys(injectedArgs).length !== injectedArgs.length
// Example value:
// [ , 'key1', , 'key2']
const injectedArgs = (0, inject_1.describeInjectedArguments)(target, method);
const extraArgs = nonInjectedArgs !== null && nonInjectedArgs !== void 0 ? nonInjectedArgs : [];
let argLength = metadata_1.DecoratorFactory.getNumberOfParameters(target, method);
// Please note `injectedArgs` contains `undefined` for non-injected args
const numberOfInjected = injectedArgs.filter(i => i != null).length;
if (argLength < numberOfInjected + extraArgs.length) {
/**
* `Function.prototype.length` excludes the rest parameter and only includes
* parameters before the first one with a default value. For example,
* `hello(@inject('name') name: string = 'John')` gives 0 for argLength
*/
argLength = numberOfInjected + extraArgs.length;
}
let nonInjectedIndex = 0;
return (0, value_promise_1.resolveList)(new Array(argLength), (val, ix) => {
// The `val` argument is not used as the resolver only uses `injectedArgs`
// and `extraArgs` to return the new value
const injection = ix < injectedArgs.length ? injectedArgs[ix] : undefined;
if (injection == null ||
(!injection.bindingSelector && !injection.resolve)) {
if (nonInjectedIndex < extraArgs.length) {
// Set the argument from the non-injected list
return extraArgs[nonInjectedIndex++];
}
else {
const name = getTargetName(target, method, ix);
throw new resolution_session_1.ResolutionError(`The argument '${name}' is not decorated for dependency injection ` +
'but no value was supplied by the caller. Did you forget to apply ' +
'@inject() to the argument?', { context: ctx, options: { session } });
}
}
return resolve(ctx, injection,
// Clone the session so that multiple arguments can be resolved in parallel
resolution_session_1.ResolutionSession.fork(session));
});
}
exports.resolveInjectedArguments = resolveInjectedArguments;
/**
* Given a class with properties decorated with `@inject`,
* return the map of properties resolved using the values
* bound in `ctx`.
* The function returns an argument array when all dependencies were
* resolved synchronously, or a Promise otherwise.
*
* @param constructor - The class for which properties should be resolved.
* @param ctx - The context containing values for `@inject` resolution
* @param session - Optional session for binding and dependency resolution
*/
function resolveInjectedProperties(constructor, ctx, session) {
/* istanbul ignore if */
if (debug.enabled) {
debug('Resolving injected properties for %s', getTargetName(constructor));
}
const injectedProperties = (0, inject_1.describeInjectedProperties)(constructor.prototype);
return (0, value_promise_1.resolveMap)(injectedProperties, injection => resolve(ctx, injection,
// Clone the session so that multiple properties can be resolved in parallel
resolution_session_1.ResolutionSession.fork(session)));
}
exports.resolveInjectedProperties = resolveInjectedProperties;
//# sourceMappingURL=resolver.js.map
;