@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
1,111 lines (1,034 loc) • 31.2 kB
text/typescript
// 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
import debugFactory from 'debug';
import {EventEmitter} from 'events';
import {bindingTemplateFor} from './binding-inspector';
import {BindingAddress, BindingKey} from './binding-key';
import {Context} from './context';
import {inspectInjections} from './inject';
import {createProxyWithInterceptors} from './interception-proxy';
import {invokeMethod} from './invocation';
import {JSONObject} from './json-types';
import {ContextTags} from './keys';
import {Provider} from './provider';
import {
asResolutionOptions,
ResolutionContext,
ResolutionError,
ResolutionOptions,
ResolutionOptionsOrSession,
ResolutionSession,
} from './resolution-session';
import {instantiateClass} from './resolver';
import {
BoundValue,
Constructor,
isPromiseLike,
MapObject,
transformValueOrPromise,
ValueOrPromise,
} from './value-promise';
const debug = debugFactory('loopback:context:binding');
/**
* Scope for binding values
*/
export enum BindingScope {
/**
* The binding provides a value that is calculated each time. This will be
* the default scope if not set.
*
* For example, with the following context hierarchy:
*
* - `app` (with a binding `'b1'` that produces sequential values 0, 1, ...)
* - req1
* - req2
*
* Now `'b1'` is resolved to a new value each time for `app` and its
* descendants `req1` and `req2`:
* - app.get('b1') ==> 0
* - req1.get('b1') ==> 1
* - req2.get('b1') ==> 2
* - req2.get('b1') ==> 3
* - app.get('b1') ==> 4
*/
TRANSIENT = 'Transient',
/**
* @deprecated Finer-grained scopes such as `APPLICATION`, `SERVER`, or
* `REQUEST` should be used instead to ensure the scope of sharing of resolved
* binding values.
*
* The binding provides a value as a singleton within each local context. The
* value is calculated only once per context and cached for subsequential
* uses. Child contexts have their own value and do not share with their
* ancestors.
*
* For example, with the following context hierarchy:
*
* - `app` (with a binding `'b1'` that produces sequential values 0, 1, ...)
* - req1
* - req2
*
* 1. `0` is the resolved value for `'b1'` within the `app` afterward
* - app.get('b1') ==> 0 (always)
*
* 2. `'b1'` is resolved in `app` but not in `req1`, a new value `1` is
* calculated and used for `req1` afterward
* - req1.get('b1') ==> 1 (always)
*
* 3. `'b1'` is resolved in `app` but not in `req2`, a new value `2` is
* calculated and used for `req2` afterward
* - req2.get('b1') ==> 2 (always)
*
*/
CONTEXT = 'Context',
/**
* The binding provides a value as a singleton within the context hierarchy
* (the owning context and its descendants). The value is calculated only
* once for the owning context and cached for subsequential uses. Child
* contexts share the same value as their ancestors.
*
* For example, with the following context hierarchy:
*
* - `app` (with a binding `'b1'` that produces sequential values 0, 1, ...)
* - req1
* - req2
*
* 1. `0` is the singleton for `app` afterward
* - app.get('b1') ==> 0 (always)
*
* 2. `'b1'` is resolved in `app`, reuse it for `req1`
* - req1.get('b1') ==> 0 (always)
*
* 3. `'b1'` is resolved in `app`, reuse it for `req2`
* - req2.get('b1') ==> 0 (always)
*/
SINGLETON = 'Singleton',
/*
* The following scopes are checked against the context hierarchy to find
* the first matching context for a given scope in the chain. Resolved binding
* values will be cached and shared on the scoped context. This ensures a
* binding to have the same value for the scoped context.
*/
/**
* Application scope
*
* @remarks
* The binding provides an application-scoped value within the context
* hierarchy. Resolved value for this binding will be cached and shared for
* the same application context (denoted by its scope property set to
* `BindingScope.APPLICATION`).
*
*/
APPLICATION = 'Application',
/**
* Server scope
*
* @remarks
* The binding provides an server-scoped value within the context hierarchy.
* Resolved value for this binding will be cached and shared for the same
* server context (denoted by its scope property set to
* `BindingScope.SERVER`).
*
* It's possible that an application has more than one servers configured,
* such as a `RestServer` and a `GrpcServer`. Both server contexts are created
* with `scope` set to `BindingScope.SERVER`. Depending on where a binding
* is resolved:
* - If the binding is resolved from the RestServer or below, it will be
* cached using the RestServer context as the key.
* - If the binding is resolved from the GrpcServer or below, it will be
* cached using the GrpcServer context as the key.
*
* The same binding can resolved/shared/cached for all servers, each of which
* has its own value for the binding.
*/
SERVER = 'Server',
/**
* Request scope
*
* @remarks
* The binding provides an request-scoped value within the context hierarchy.
* Resolved value for this binding will be cached and shared for the same
* request context (denoted by its scope property set to
* `BindingScope.REQUEST`).
*
* The `REQUEST` scope is very useful for controllers, services and artifacts
* that want to have a single instance/value for a given request.
*/
REQUEST = 'Request',
}
/**
* Type of the binding source
*/
export enum BindingType {
/**
* A fixed value
*/
CONSTANT = 'Constant',
/**
* A function to get the value
*/
DYNAMIC_VALUE = 'DynamicValue',
/**
* A class to be instantiated as the value
*/
CLASS = 'Class',
/**
* A provider class with `value()` function to get the value
*/
PROVIDER = 'Provider',
/**
* A alias to another binding key with optional path
*/
ALIAS = 'Alias',
}
/**
* Binding source for `to`
*/
export type ConstantBindingSource<T> = {
type: BindingType.CONSTANT;
value: T;
};
/**
* Binding source for `toDynamicValue`
*/
export type DynamicValueBindingSource<T> = {
type: BindingType.DYNAMIC_VALUE;
value: ValueFactory<T> | DynamicValueProviderClass<T>;
};
/**
* Binding source for `toClass`
*/
export type ClassBindingSource<T> = {
type: BindingType.CLASS;
value: Constructor<T>;
};
/**
* Binding source for `toProvider`
*/
export type ProviderBindingSource<T> = {
type: BindingType.PROVIDER;
value: Constructor<Provider<T>>;
};
/**
* Binding source for `toAlias`
*/
export type AliasBindingSource<T> = {
type: BindingType.ALIAS;
value: BindingAddress<T>;
};
/**
* Source for the binding, including the type and value
*/
export type BindingSource<T> =
| ConstantBindingSource<T>
| DynamicValueBindingSource<T>
| ClassBindingSource<T>
| ProviderBindingSource<T>
| AliasBindingSource<T>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type TagMap = MapObject<any>;
/**
* Binding tag can be a simple name or name/value pairs
*/
export type BindingTag = TagMap | string;
/**
* A function as the template to configure bindings
*/
export type BindingTemplate<T = unknown> = (binding: Binding<T>) => void;
/**
* Information for a binding event
*/
export type BindingEvent = {
/**
* Event type
*/
type: 'changed' | string;
/**
* Source binding that emits the event
*/
binding: Readonly<Binding<unknown>>;
/**
* Operation that triggers the event
*/
operation: 'tag' | 'scope' | 'value' | string;
};
/**
* Event listeners for binding events
*/
export type BindingEventListener = (
/**
* Binding event
*/
event: BindingEvent,
) => void;
/**
* A factory function for `toDynamicValue`
*/
export type ValueFactory<T = unknown> = (
resolutionCtx: ResolutionContext,
) => ValueOrPromise<T | undefined>;
/**
* A class with a static `value` method as the factory function for
* `toDynamicValue`.
*
* @example
* ```ts
* import {inject} from '@loopback/context';
*
* export class DynamicGreetingProvider {
* static value(@inject('currentUser') user: string) {
* return `Hello, ${user}`;
* }
* }
* ```
*/
export interface DynamicValueProviderClass<T = unknown>
extends Constructor<unknown>,
Function {
value: (...args: BoundValue[]) => ValueOrPromise<T>;
}
/**
* Adapt the ValueFactoryProvider class to be a value factory
* @param provider - ValueFactoryProvider class
*/
function toValueFactory<T = unknown>(
provider: DynamicValueProviderClass<T>,
): ValueFactory<T> {
return resolutionCtx =>
invokeMethod(provider, 'value', resolutionCtx.context, [], {
skipInterceptors: true,
session: resolutionCtx.options.session,
});
}
/**
* Check if the factory is a value factory provider class
* @param factory - A factory function or a dynamic value provider class
*/
export function isDynamicValueProviderClass<T = unknown>(
factory: unknown,
): factory is DynamicValueProviderClass<T> {
// Not a class
if (typeof factory !== 'function' || !String(factory).startsWith('class ')) {
return false;
}
const valueMethod = (factory as DynamicValueProviderClass).value;
return typeof valueMethod === 'function';
}
/**
* Binding represents an entry in the `Context`. Each binding has a key and a
* corresponding value getter.
*/
export class Binding<T = BoundValue> extends EventEmitter {
/**
* Key of the binding
*/
public readonly key: string;
/**
* Map for tag name/value pairs
*/
public readonly tagMap: TagMap = {};
private _scope?: BindingScope;
/**
* Scope of the binding to control how the value is cached/shared
*/
public get scope(): BindingScope {
// Default to TRANSIENT if not set
return this._scope ?? BindingScope.TRANSIENT;
}
/**
* Type of the binding value getter
*/
public get type(): BindingType | undefined {
return this._source?.type;
}
private _cache: WeakMap<Context, ValueOrPromise<T>>;
private _getValue?: ValueFactory<T>;
/**
* The original source value received from `to`, `toClass`, `toDynamicValue`,
* `toProvider`, or `toAlias`.
*/
private _source?: BindingSource<T>;
public get source() {
return this._source;
}
/**
* For bindings bound via `toClass()`, this property contains the constructor
* function of the class
*/
public get valueConstructor(): Constructor<T> | undefined {
return this._source?.type === BindingType.CLASS
? this._source?.value
: undefined;
}
/**
* For bindings bound via `toProvider()`, this property contains the
* constructor function of the provider class
*/
public get providerConstructor(): Constructor<Provider<T>> | undefined {
return this._source?.type === BindingType.PROVIDER
? this._source?.value
: undefined;
}
constructor(
key: BindingAddress<T>,
public isLocked: boolean = false,
) {
super();
BindingKey.validate(key);
this.key = key.toString();
}
/**
* Cache the resolved value by the binding scope
* @param resolutionCtx - The resolution context
* @param result - The calculated value for the binding
*/
private _cacheValue(
resolutionCtx: Context,
result: ValueOrPromise<T>,
): ValueOrPromise<T> {
// Initialize the cache as a weakmap keyed by context
if (!this._cache) this._cache = new WeakMap<Context, ValueOrPromise<T>>();
if (this.scope !== BindingScope.TRANSIENT) {
this._cache.set(resolutionCtx!, result);
}
return result;
}
/**
* Clear the cache
*/
private _clearCache() {
if (!this._cache) return;
// WeakMap does not have a `clear` method
this._cache = new WeakMap();
}
/**
* Invalidate the binding cache so that its value will be reloaded next time.
* This is useful to force reloading a cached value when its configuration or
* dependencies are changed.
* **WARNING**: The state held in the cached value will be gone.
*
* @param ctx - Context object
*/
refresh(ctx: Context) {
if (!this._cache) return;
if (this.scope !== BindingScope.TRANSIENT) {
const resolutionCtx = ctx.getResolutionContext(this);
if (resolutionCtx != null) {
this._cache.delete(resolutionCtx);
}
}
}
/**
* This is an internal function optimized for performance.
* Users should use `@inject(key)` or `ctx.get(key)` instead.
*
* Get the value bound to this key. Depending on `isSync`, this
* function returns either:
* - the bound value
* - a promise of the bound value
*
* Consumers wishing to consume sync values directly should use `isPromiseLike`
* to check the type of the returned value to decide how to handle it.
*
* @example
* ```
* const result = binding.getValue(ctx);
* if (isPromiseLike(result)) {
* result.then(doSomething)
* } else {
* doSomething(result);
* }
* ```
*
* @param ctx - Context for the resolution
* @param session - Optional session for binding and dependency resolution
*/
getValue(ctx: Context, session?: ResolutionSession): ValueOrPromise<T>;
/**
* Returns a value or promise for this binding in the given context. The
* resolved value can be `undefined` if `optional` is set to `true` in
* `options`.
* @param ctx - Context for the resolution
* @param options - Optional options for binding and dependency resolution
*/
getValue(
ctx: Context,
options?: ResolutionOptions,
): ValueOrPromise<T | undefined>;
// Implementation
getValue(
ctx: Context,
optionsOrSession?: ResolutionOptionsOrSession,
): ValueOrPromise<T | undefined> {
/* istanbul ignore if */
if (debug.enabled) {
debug('Get value for binding %s', this.key);
}
const options = asResolutionOptions(optionsOrSession);
const resolutionCtx = this.getResolutionContext(ctx, options);
if (resolutionCtx == null) return undefined;
// Keep a snapshot for proxy
const savedSession =
ResolutionSession.fork(options.session) ?? new ResolutionSession();
// First check cached value for non-transient
if (this._cache) {
if (this.scope !== BindingScope.TRANSIENT) {
if (resolutionCtx && this._cache.has(resolutionCtx)) {
const value = this._cache.get(resolutionCtx)!;
return this.getValueOrProxy(
resolutionCtx,
{...options, session: savedSession},
value,
);
}
}
}
const resolutionMetadata = {
context: resolutionCtx!,
binding: this,
options,
};
if (typeof this._getValue === 'function') {
const result = ResolutionSession.runWithBinding(
s => {
const optionsWithSession = {
...options,
session: s,
// Force to be the non-proxy version
asProxyWithInterceptors: false,
};
// We already test `this._getValue` is a function. It's safe to assert
// that `this._getValue` is not undefined.
return this._getValue!({
...resolutionMetadata,
options: optionsWithSession,
});
},
this,
options.session,
);
const value = this._cacheValue(resolutionCtx!, result);
return this.getValueOrProxy(
resolutionCtx,
{...options, session: savedSession},
value,
);
}
// `@inject.binding` adds a binding without _getValue
if (options.optional) return undefined;
return Promise.reject(
new ResolutionError(
`No value was configured for binding ${this.key}.`,
resolutionMetadata,
),
);
}
private getValueOrProxy(
resolutionCtx: Context,
options: ResolutionOptions,
value: ValueOrPromise<T>,
): ValueOrPromise<T> {
const session = options.session!;
session.pushBinding(this);
return Binding.valueOrProxy(
{
context: resolutionCtx,
binding: this,
options,
},
value,
);
}
/**
* Locate and validate the resolution context
* @param ctx - Current context
* @param options - Resolution options
*/
private getResolutionContext(ctx: Context, options: ResolutionOptions) {
const resolutionCtx = ctx.getResolutionContext(this);
switch (this.scope) {
case BindingScope.APPLICATION:
case BindingScope.SERVER:
case BindingScope.REQUEST:
if (resolutionCtx == null) {
const msg =
`Binding "${this.key}" in context "${ctx.name}" cannot` +
` be resolved in scope "${this.scope}"`;
if (options.optional) {
debug(msg);
return undefined;
}
throw new Error(msg);
}
}
const ownerCtx = ctx.getOwnerContext(this.key);
if (ownerCtx != null && !ownerCtx.isVisibleTo(resolutionCtx!)) {
const msg =
`Resolution context "${resolutionCtx?.name}" does not have ` +
`visibility to binding "${this.key} (scope:${this.scope})" in context "${ownerCtx.name}"`;
if (options.optional) {
debug(msg);
return undefined;
}
throw new Error(msg);
}
return resolutionCtx;
}
/**
* Lock the binding so that it cannot be rebound
*/
lock(): this {
this.isLocked = true;
return this;
}
/**
* Emit a `changed` event
* @param operation - Operation that makes changes
*/
private emitChangedEvent(operation: string) {
const event: BindingEvent = {binding: this, operation, type: 'changed'};
this.emit('changed', event);
}
/**
* Tag the binding with names or name/value objects. A tag has a name and
* an optional value. If not supplied, the tag name is used as the value.
*
* @param tags - A list of names or name/value objects. Each
* parameter can be in one of the following forms:
* - string: A tag name without value
* - string[]: An array of tag names
* - TagMap: A map of tag name/value pairs
*
* @example
* ```ts
* // Add a named tag `controller`
* binding.tag('controller');
*
* // Add two named tags: `controller` and `rest`
* binding.tag('controller', 'rest');
*
* // Add two tags
* // - `controller` (name = 'controller')
* // `{name: 'my-controller'}` (name = 'name', value = 'my-controller')
* binding.tag('controller', {name: 'my-controller'});
*
* ```
*/
tag(...tags: BindingTag[]): this {
for (const t of tags) {
if (typeof t === 'string') {
this.tagMap[t] = t;
} else if (Array.isArray(t)) {
// Throw an error as TypeScript cannot exclude array from TagMap
throw new Error(
'Tag must be a string or an object (but not array): ' + t,
);
} else {
Object.assign(this.tagMap, t);
}
}
this.emitChangedEvent('tag');
return this;
}
/**
* Get an array of tag names
*/
get tagNames() {
return Object.keys(this.tagMap);
}
/**
* Set the binding scope
* @param scope - Binding scope
*/
inScope(scope: BindingScope): this {
if (this._scope !== scope) this._clearCache();
this._scope = scope;
this.emitChangedEvent('scope');
return this;
}
/**
* Apply default scope to the binding. It only changes the scope if it's not
* set yet
* @param scope - Default binding scope
*/
applyDefaultScope(scope: BindingScope): this {
if (!this._scope) {
this.inScope(scope);
}
return this;
}
/**
* Set the `_getValue` function
* @param getValue - getValue function
*/
private _setValueGetter(getValue: ValueFactory<T>) {
// Clear the cache
this._clearCache();
this._getValue = resolutionCtx => {
return getValue(resolutionCtx);
};
this.emitChangedEvent('value');
}
/**
* Bind the key to a constant value. The value must be already available
* at binding time, it is not allowed to pass a Promise instance.
*
* @param value - The bound value.
*
* @example
*
* ```ts
* ctx.bind('appName').to('CodeHub');
* ```
*/
to(value: T): this {
if (isPromiseLike(value)) {
// Promises are a construct primarily intended for flow control:
// In an algorithm with steps 1 and 2, we want to wait for the outcome
// of step 1 before starting step 2.
//
// Promises are NOT a tool for storing values that may become available
// in the future, depending on the success or a failure of a background
// async task.
//
// Values stored in bindings are typically accessed only later,
// in a different turn of the event loop or the Promise micro-queue.
// As a result, when a promise is stored via `.to()` and is rejected
// later, then more likely than not, there will be no error (catch)
// handler registered yet, and Node.js will print
// "Unhandled Rejection Warning".
throw new Error(
'Promise instances are not allowed for constant values ' +
'bound via ".to()". Register an async getter function ' +
'via ".toDynamicValue()" instead.',
);
}
/* istanbul ignore if */
if (debug.enabled) {
debug('Bind %s to constant:', this.key, value);
}
this._source = {
type: BindingType.CONSTANT,
value,
};
this._setValueGetter(resolutionCtx => {
return Binding.valueOrProxy(resolutionCtx, value);
});
return this;
}
/**
* Bind the key to a computed (dynamic) value.
*
* @param factoryFn - The factory function creating the value.
* Both sync and async functions are supported.
*
* @example
*
* ```ts
* // synchronous
* ctx.bind('now').toDynamicValue(() => Date.now());
*
* // asynchronous
* ctx.bind('something').toDynamicValue(
* async () => Promise.delay(10).then(doSomething)
* );
* ```
*/
toDynamicValue(
factory: ValueFactory<T> | DynamicValueProviderClass<T>,
): this {
/* istanbul ignore if */
if (debug.enabled) {
debug('Bind %s to dynamic value:', this.key, factory);
}
this._source = {
type: BindingType.DYNAMIC_VALUE,
value: factory,
};
let factoryFn: ValueFactory<T>;
if (isDynamicValueProviderClass(factory)) {
factoryFn = toValueFactory(factory);
} else {
factoryFn = factory;
}
this._setValueGetter(resolutionCtx => {
const value = factoryFn(resolutionCtx);
return Binding.valueOrProxy(resolutionCtx, value);
});
return this;
}
private static valueOrProxy<V>(
resolutionCtx: ResolutionContext,
value: ValueOrPromise<V>,
) {
if (!resolutionCtx.options.asProxyWithInterceptors) return value;
return createInterceptionProxyFromInstance(
value,
resolutionCtx.context,
resolutionCtx.options.session,
);
}
/**
* Bind the key to a value computed by a Provider.
*
* * @example
*
* ```ts
* export class DateProvider implements Provider<Date> {
* constructor(@inject('stringDate') private param: String){}
* value(): Date {
* return new Date(param);
* }
* }
* ```
*
* @param provider - The value provider to use.
*/
toProvider(providerClass: Constructor<Provider<T>>): this {
/* istanbul ignore if */
if (debug.enabled) {
debug('Bind %s to provider %s', this.key, providerClass.name);
}
this._source = {
type: BindingType.PROVIDER,
value: providerClass,
};
this._setValueGetter(resolutionCtx => {
const providerOrPromise = instantiateClass<Provider<T>>(
providerClass,
resolutionCtx.context,
resolutionCtx.options.session,
);
const value = transformValueOrPromise(providerOrPromise, p => p.value());
return Binding.valueOrProxy(resolutionCtx, value);
});
return this;
}
/**
* Bind the key to an instance of the given class.
*
* @param ctor - The class constructor to call. Any constructor
* arguments must be annotated with `@inject` so that
* we can resolve them from the context.
*/
toClass<C extends T & object>(ctor: Constructor<C>): this {
/* istanbul ignore if */
if (debug.enabled) {
debug('Bind %s to class %s', this.key, ctor.name);
}
this._source = {
type: BindingType.CLASS,
value: ctor,
};
this._setValueGetter(resolutionCtx => {
const value = instantiateClass(
ctor,
resolutionCtx.context,
resolutionCtx.options.session,
);
return Binding.valueOrProxy(resolutionCtx, value);
});
return this;
}
/**
* Bind to a class optionally decorated with `@injectable`. Based on the
* introspection of the class, it calls `toClass/toProvider/toDynamicValue`
* internally. The current binding key will be preserved (not being overridden
* by the key inferred from the class or options).
*
* This is similar to {@link createBindingFromClass} but applies to an
* existing binding.
*
* @example
*
* ```ts
* @injectable({scope: BindingScope.SINGLETON, tags: {service: 'MyService}})
* class MyService {
* // ...
* }
*
* const ctx = new Context();
* ctx.bind('services.MyService').toInjectable(MyService);
* ```
*
* @param ctor - A class decorated with `@injectable`.
*/
toInjectable(
ctor: DynamicValueProviderClass<T> | Constructor<T | Provider<T>>,
) {
this.apply(bindingTemplateFor(ctor));
return this;
}
/**
* Bind the key to an alias of another binding
* @param keyWithPath - Target binding key with optional path,
* such as `servers.RestServer.options#apiExplorer`
*/
toAlias(keyWithPath: BindingAddress<T>) {
/* istanbul ignore if */
if (debug.enabled) {
debug('Bind %s to alias %s', this.key, keyWithPath);
}
this._source = {
type: BindingType.ALIAS,
value: keyWithPath,
};
this._setValueGetter(({context, options}) => {
return context.getValueOrPromise(keyWithPath, options);
});
return this;
}
/**
* Unlock the binding
*/
unlock(): this {
this.isLocked = false;
return this;
}
/**
* Apply one or more template functions to set up the binding with scope,
* tags, and other attributes as a group.
*
* @example
* ```ts
* const serverTemplate = (binding: Binding) =>
* binding.inScope(BindingScope.SINGLETON).tag('server');
*
* const serverBinding = new Binding<RestServer>('servers.RestServer1');
* serverBinding.apply(serverTemplate);
* ```
* @param templateFns - One or more functions to configure the binding
*/
apply(...templateFns: BindingTemplate<T>[]): this {
for (const fn of templateFns) {
fn(this);
}
return this;
}
/**
* Convert to a plain JSON object
*/
toJSON(): JSONObject {
const json: JSONObject = {
key: this.key,
scope: this.scope,
tags: this.tagMap,
isLocked: this.isLocked,
};
if (this.type != null) {
json.type = this.type;
}
switch (this._source?.type) {
case BindingType.CLASS:
json.valueConstructor = this._source?.value.name;
break;
case BindingType.PROVIDER:
json.providerConstructor = this._source?.value.name;
break;
case BindingType.ALIAS:
json.alias = this._source?.value.toString();
break;
}
return json;
}
/**
* Inspect the binding to return a json representation of the binding information
* @param options - Options to control what information should be included
*/
inspect(options: BindingInspectOptions = {}): JSONObject {
options = {
includeInjections: false,
...options,
};
const json = this.toJSON();
if (options.includeInjections) {
const injections = inspectInjections(this);
if (Object.keys(injections).length) json.injections = injections;
}
return json;
}
/**
* A static method to create a binding so that we can do
* `Binding.bind('foo').to('bar');` as `new Binding('foo').to('bar')` is not
* easy to read.
* @param key - Binding key
*/
static bind<V = unknown>(key: BindingAddress<V>): Binding<V> {
return new Binding(key);
}
/**
* Create a configuration binding for the given key
*
* @example
* ```ts
* const configBinding = Binding.configure('servers.RestServer.server1')
* .to({port: 3000});
* ```
*
* @typeParam V Generic type for the configuration value (not the binding to
* be configured)
*
* @param key - Key for the binding to be configured
*/
static configure<V = unknown>(key: BindingAddress): Binding<V> {
return new Binding(BindingKey.buildKeyForConfig<V>(key)).tag({
[ContextTags.CONFIGURATION_FOR]: key.toString(),
});
}
/**
* The "changed" event is emitted by methods such as `tag`, `inScope`, `to`,
* and `toClass`.
*
* @param eventName The name of the event - always `changed`.
* @param listener The listener function to call when the event is emitted.
*/
on(eventName: 'changed', listener: BindingEventListener): this;
// The generic variant inherited from EventEmitter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(event: string | symbol, listener: (...args: any[]) => void): this;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(event: string | symbol, listener: (...args: any[]) => void): this {
return super.on(event, listener);
}
/**
* The "changed" event is emitted by methods such as `tag`, `inScope`, `to`,
* and `toClass`.
*
* @param eventName The name of the event - always `changed`.
* @param listener The listener function to call when the event is emitted.
*/
once(eventName: 'changed', listener: BindingEventListener): this;
// The generic variant inherited from EventEmitter
// eslint-disable-next-line @typescript-eslint/no-explicit-any
once(event: string | symbol, listener: (...args: any[]) => void): this;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
once(event: string | symbol, listener: (...args: any[]) => void): this {
return super.once(event, listener);
}
}
/**
* Options for binding.inspect()
*/
export interface BindingInspectOptions {
/**
* The flag to control if injections should be inspected
*/
includeInjections?: boolean;
}
function createInterceptionProxyFromInstance<T>(
instOrPromise: ValueOrPromise<T>,
context: Context,
session?: ResolutionSession,
) {
return transformValueOrPromise(instOrPromise, inst => {
if (typeof inst !== 'object' || inst == null) return inst;
return createProxyWithInterceptors(
// Cast inst from `T` to `object`
inst as unknown as object,
context,
session,
) as unknown as T;
});
}