// Copyright IBM Corp. and LoopBack contributors 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 import {BindingFilter} from './binding-filter'; import {BindingAddress, BindingKey} from './binding-key'; import {Context} from './context'; import {ContextView} from './context-view'; import {assertTargetType, inject, Injection, InjectionMetadata} from './inject'; import {ResolutionSession} from './resolution-session'; import {getDeepProperty, ValueOrPromise} from './value-promise'; /** * Injection metadata for `@config.*` */ export interface ConfigInjectionMetadata extends InjectionMetadata { /** * Property path to retrieve the configuration of the target binding, for * example, `rest.host`. */ propertyPath?: string; /** * Customize the target binding key from which the configuration is fetched. * If not specified, the configuration of the current binding that contains * the injection is used. */ fromBinding?: BindingAddress; } /** * Inject a property from `config` of the current binding. If no corresponding * config value is present, `undefined` will be injected as the configuration * binding is resolved with `optional: true` by default. * * @example * ```ts * class Store { * constructor( * @config('x') public optionX: number, * @config('y') public optionY: string, * ) { } * } * * ctx.configure('store1', { x: 1, y: 'a' }); * ctx.configure('store2', { x: 2, y: 'b' }); * * ctx.bind('store1').toClass(Store); * ctx.bind('store2').toClass(Store); * * const store1 = ctx.getSync('store1'); * expect(store1.optionX).to.eql(1); * expect(store1.optionY).to.eql('a'); * * const store2 = ctx.getSync('store2'); * expect(store2.optionX).to.eql(2); * expect(store2.optionY).to.eql('b'); * ``` * * @param propertyPath - Optional property path of the config. If is `''` or not * present, the `config` object will be returned. * @param metadata - Optional metadata to help the injection */ export function config( propertyPath?: string | ConfigInjectionMetadata, metadata?: ConfigInjectionMetadata, ) { propertyPath = propertyPath ?? ''; if (typeof propertyPath === 'object') { metadata = propertyPath; propertyPath = ''; } metadata = Object.assign( {propertyPath, decorator: '@config', optional: true}, metadata, ); return inject('', metadata, resolveFromConfig); } export namespace config { /** * `@inject.getter` decorator to inject a config getter function * @param propertyPath - Optional property path of the config object * @param metadata - Injection metadata */ export const getter = function injectConfigGetter( propertyPath?: string | ConfigInjectionMetadata, metadata?: ConfigInjectionMetadata, ) { propertyPath = propertyPath ?? ''; if (typeof propertyPath === 'object') { metadata = propertyPath; propertyPath = ''; } metadata = Object.assign( {propertyPath, decorator: '@config.getter', optional: true}, metadata, ); return inject('', metadata, resolveAsGetterFromConfig); }; /** * `@inject.view` decorator to inject a config context view to allow dynamic * changes in configuration * @param propertyPath - Optional property path of the config object * @param metadata - Injection metadata */ export const view = function injectConfigView( propertyPath?: string | ConfigInjectionMetadata, metadata?: ConfigInjectionMetadata, ) { propertyPath = propertyPath ?? ''; if (typeof propertyPath === 'object') { metadata = propertyPath; propertyPath = ''; } metadata = Object.assign( {propertyPath, decorator: '@config.view', optional: true}, metadata, ); return inject('', metadata, resolveAsViewFromConfig); }; } /** * Get the key for the current binding on which dependency injection is * performed * @param session - Resolution session */ function getCurrentBindingKey(session: ResolutionSession) { // The current binding is not set if `instantiateClass` is invoked directly return session.currentBinding?.key; } /** * Get the target binding key from which the configuration should be resolved * @param injection - Injection * @param session - Resolution session */ function getTargetBindingKey(injection: Injection, session: ResolutionSession) { return injection.metadata.fromBinding || getCurrentBindingKey(session); } /** * Resolver for `@config` * @param ctx - Context object * @param injection - Injection metadata * @param session - Resolution session */ function resolveFromConfig( ctx: Context, injection: Injection, session: ResolutionSession, ): ValueOrPromise { const bindingKey = getTargetBindingKey(injection, session); // Return `undefined` if no current binding is present if (!bindingKey) return undefined; const meta = injection.metadata; return ctx.getConfigAsValueOrPromise(bindingKey, meta.propertyPath, { session, optional: meta.optional, }); } /** * Resolver from `@config.getter` * @param ctx - Context object * @param injection - Injection metadata * @param session - Resolution session */ function resolveAsGetterFromConfig( ctx: Context, injection: Injection, session: ResolutionSession, ) { assertTargetType(injection, Function, 'Getter function'); const bindingKey = getTargetBindingKey(injection, session); const meta = injection.metadata; return async function getter() { // Return `undefined` if no current binding is present if (!bindingKey) return undefined; return ctx.getConfigAsValueOrPromise(bindingKey, meta.propertyPath, { // 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, optional: meta.optional, }); }; } /** * Resolver for `@config.view` * @param ctx - Context object * @param injection - Injection metadata * @param session - Resolution session */ function resolveAsViewFromConfig( ctx: Context, injection: Injection, session: ResolutionSession, ) { assertTargetType(injection, ContextView); const bindingKey = getTargetBindingKey(injection, session); // Return `undefined` if no current binding is present if (!bindingKey) return undefined; const view = new ConfigView( ctx, binding => binding.key === BindingKey.buildKeyForConfig(bindingKey).toString(), injection.metadata.propertyPath, ); view.open(); return view; } /** * A subclass of `ContextView` to handle dynamic configuration as its * `values()` honors the `propertyPath`. */ class ConfigView extends ContextView { constructor( ctx: Context, filter: BindingFilter, private propertyPath?: string, ) { super(ctx, filter); } /** * Get values for the configuration with a property path * @param session - Resolution session */ async values(session?: ResolutionSession) { const configValues = await super.values(session); const propertyPath = this.propertyPath; if (!propertyPath) return configValues; return configValues.map(v => getDeepProperty(v, propertyPath)); } }