diff --git a/docs/site/Binding.md b/docs/site/Binding.md index 5ba0cc0e1b0b..111c464b6f76 100644 --- a/docs/site/Binding.md +++ b/docs/site/Binding.md @@ -113,6 +113,21 @@ class MyValueProvider implements Provider { binding.toProvider(MyValueProvider); ``` +#### An alias + +An alias is the key with optional path to resolve the value from another +binding. For example, if we want to get options from RestServer for the API +explorer, we can configure the `apiExplorer.options` to be resolved from +`servers.RestServer.options#apiExplorer`. + +```ts +ctx.bind('servers.RestServer.options').to({apiExplorer: {path: '/explorer'}}); +ctx + .bind('apiExplorer.options') + .toAlias('servers.RestServer.options#apiExplorer'); +const apiExplorerOptions = await ctx.get('apiExplorer.options'); // => {path: '/explorer'} +``` + ### Configure the scope We allow a binding to be resolved within a context using one of the following diff --git a/packages/context/src/__tests__/unit/binding.unit.ts b/packages/context/src/__tests__/unit/binding.unit.ts index 28cb6837a4d0..1ce6aaa4fd56 100644 --- a/packages/context/src/__tests__/unit/binding.unit.ts +++ b/packages/context/src/__tests__/unit/binding.unit.ts @@ -191,6 +191,57 @@ describe('Binding', () => { }); }); + describe('toAlias(bindingKeyWithPath)', async () => { + it('binds to another binding with sync value', () => { + ctx.bind('parent.options').to({child: {disabled: true}}); + ctx.bind('child.options').toAlias('parent.options#child'); + const childOptions = ctx.getSync('child.options'); + expect(childOptions).to.eql({disabled: true}); + }); + + it('binds to another binding with async value', async () => { + ctx + .bind('parent.options') + .toDynamicValue(() => Promise.resolve({child: {disabled: true}})); + ctx.bind('child.options').toAlias('parent.options#child'); + const childOptions = await ctx.get('child.options'); + expect(childOptions).to.eql({disabled: true}); + }); + + it('reports error if alias binding cannot be resolved', () => { + ctx.bind('child.options').toAlias('parent.options#child'); + expect(() => ctx.getSync('child.options')).to.throw( + /The key 'parent.options' is not bound to any value in context/, + ); + }); + + it('reports error if alias binding cannot be resolved - async', async () => { + ctx.bind('child.options').toAlias('parent.options#child'); + return expect(ctx.get('child.options')).to.be.rejectedWith( + /The key 'parent.options' is not bound to any value in context/, + ); + }); + + it('allows optional if alias binding cannot be resolved', () => { + ctx.bind('child.options').toAlias('parent.options#child'); + const childOptions = ctx.getSync('child.options', {optional: true}); + expect(childOptions).to.be.undefined(); + }); + + it('allows optional if alias binding cannot be resolved - async', async () => { + ctx.bind('child.options').toAlias('parent.options#child'); + const childOptions = await ctx.get('child.options', {optional: true}); + expect(childOptions).to.be.undefined(); + }); + + it('sets type to ALIAS', () => { + const childBinding = ctx + .bind('child.options') + .toAlias('parent.options#child'); + expect(childBinding.type).to.equal(BindingType.ALIAS); + }); + }); + describe('apply(templateFn)', () => { it('applies a template function', async () => { binding.apply(b => { diff --git a/packages/context/src/binding.ts b/packages/context/src/binding.ts index 3045bdee3f7c..a19648e28fdf 100644 --- a/packages/context/src/binding.ts +++ b/packages/context/src/binding.ts @@ -7,7 +7,11 @@ import * as debugModule from 'debug'; import {BindingAddress, BindingKey} from './binding-key'; import {Context} from './context'; import {Provider} from './provider'; -import {ResolutionSession} from './resolution-session'; +import { + asResolutionOptions, + ResolutionOptionsOrSession, + ResolutionSession, +} from './resolution-session'; import {instantiateClass} from './resolver'; import { BoundValue, @@ -113,6 +117,10 @@ export enum BindingType { * A provider class with `value()` function to get the value */ PROVIDER = 'Provider', + /** + * A alias to another binding key with optional path + */ + ALIAS = 'Alias', } // tslint:disable-next-line:no-any @@ -162,9 +170,9 @@ export class Binding { private _cache: WeakMap; private _getValue: ( - ctx?: Context, - session?: ResolutionSession, - ) => ValueOrPromise; + ctx: Context, + optionsOrSession?: ResolutionOptionsOrSession, + ) => ValueOrPromise; private _valueConstructor?: Constructor; /** @@ -228,7 +236,10 @@ export class Binding { * @param ctx Context for the resolution * @param session Optional session for binding and dependency resolution */ - getValue(ctx: Context, session?: ResolutionSession): ValueOrPromise { + getValue( + ctx: Context, + optionsOrSession?: ResolutionOptionsOrSession, + ): ValueOrPromise { /* istanbul ignore if */ if (debug.enabled) { debug('Get value for binding %s', this.key); @@ -246,11 +257,15 @@ export class Binding { } } } + optionsOrSession = asResolutionOptions(optionsOrSession); if (this._getValue) { let result = ResolutionSession.runWithBinding( - s => this._getValue(ctx, s), + s => { + const options = Object.assign({}, optionsOrSession, {session: s}); + return this._getValue(ctx, options); + }, this, - session, + optionsOrSession.session, ); return this._cacheValue(ctx, result); } @@ -426,11 +441,11 @@ export class Binding { debug('Bind %s to provider %s', this.key, providerClass.name); } this._type = BindingType.PROVIDER; - this._getValue = (ctx, session) => { + this._getValue = (ctx, optionsOrSession) => { const providerOrPromise = instantiateClass>( providerClass, - ctx!, - session, + ctx, + asResolutionOptions(optionsOrSession).session, ); return transformValueOrPromise(providerOrPromise, p => p.value()); }; @@ -450,11 +465,44 @@ export class Binding { debug('Bind %s to class %s', this.key, ctor.name); } this._type = BindingType.CLASS; - this._getValue = (ctx, session) => instantiateClass(ctor, ctx!, session); + this._getValue = (ctx, optionsOrSession) => + instantiateClass( + ctor, + ctx, + asResolutionOptions(optionsOrSession).session, + ); this._valueConstructor = 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) { + /* istanbul ignore if */ + if (debug.enabled) { + debug('Bind %s to alias %s', this.key, keyWithPath); + } + this._type = BindingType.ALIAS; + this._getValue = (ctx, optionsOrSession) => { + const options = asResolutionOptions(optionsOrSession); + const valueOrPromise = ctx.getValueOrPromise(keyWithPath, options); + return transformValueOrPromise(valueOrPromise, val => { + if (val === undefined && !options.optional) { + throw new Error( + `No value was configured for binding ${keyWithPath}.`, + ); + } else return val; + }); + }; + return this; + } + + /** + * Unlock the binding + */ unlock(): this { this.isLocked = false; return this; diff --git a/packages/context/src/context.ts b/packages/context/src/context.ts index 3d32c221e321..dfc3f3e31556 100644 --- a/packages/context/src/context.ts +++ b/packages/context/src/context.ts @@ -6,11 +6,9 @@ import * as debugModule from 'debug'; import {EventEmitter} from 'events'; import {v1 as uuidv1} from 'uuid'; -import {ValueOrPromise} from '.'; import {Binding, BindingTag} from './binding'; import {BindingFilter, filterByKey, filterByTag} from './binding-filter'; import {BindingAddress, BindingKey} from './binding-key'; -import {ContextView} from './context-view'; import { ContextEventObserver, ContextEventType, @@ -18,8 +16,19 @@ import { Notification, Subscription, } from './context-observer'; -import {ResolutionOptions, ResolutionSession} from './resolution-session'; -import {BoundValue, getDeepProperty, isPromiseLike} from './value-promise'; +import {ContextView} from './context-view'; +import { + asResolutionOptions, + ResolutionOptions, + ResolutionOptionsOrSession, + ResolutionSession, +} from './resolution-session'; +import { + BoundValue, + getDeepProperty, + isPromiseLike, + ValueOrPromise, +} from './value-promise'; /** * Polyfill Symbol.asyncIterator as required by TypeScript for Node 8.x. @@ -766,22 +775,16 @@ export class Context extends EventEmitter { */ getValueOrPromise( keyWithPath: BindingAddress, - optionsOrSession?: ResolutionOptions | ResolutionSession, + optionsOrSession?: ResolutionOptionsOrSession, ): ValueOrPromise { const {key, propertyPath} = BindingKey.parseKeyWithPath(keyWithPath); - // backwards compatibility - if (optionsOrSession instanceof ResolutionSession) { - optionsOrSession = {session: optionsOrSession}; - } + optionsOrSession = asResolutionOptions(optionsOrSession); const binding = this.getBinding(key, optionsOrSession); if (binding == null) return undefined; - const boundValue = binding.getValue( - this, - optionsOrSession && optionsOrSession.session, - ); + const boundValue = binding.getValue(this, optionsOrSession); if (propertyPath === undefined || propertyPath === '') { return boundValue; } diff --git a/packages/context/src/resolution-session.ts b/packages/context/src/resolution-session.ts index 4bfc53cfa19b..edef86ec9216 100644 --- a/packages/context/src/resolution-session.ts +++ b/packages/context/src/resolution-session.ts @@ -3,19 +3,15 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT +import {DecoratorFactory} from '@loopback/metadata'; +import * as debugModule from 'debug'; import {Binding} from './binding'; import {Injection} from './inject'; -import {ValueOrPromise, BoundValue, tryWithFinally} from './value-promise'; -import * as debugModule from 'debug'; -import {DecoratorFactory} from '@loopback/metadata'; +import {BoundValue, tryWithFinally, ValueOrPromise} from './value-promise'; const debugSession = debugModule('loopback:context:resolver:session'); const getTargetName = DecoratorFactory.getTargetName; -// NOTE(bajtos) The following import is required to satisfy TypeScript compiler -// tslint:disable-next-line:no-unused -import {BindingKey} from './binding-key'; - /** * A function to be executed with the resolution session */ @@ -343,3 +339,22 @@ export interface ResolutionOptions { */ optional?: boolean; } + +/** + * Resolution options or session + */ +export type ResolutionOptionsOrSession = ResolutionOptions | ResolutionSession; + +/** + * Normalize ResolutionOptionsOrSession to ResolutionOptions + * @param optionsOrSession resolution options or session + */ +export function asResolutionOptions( + optionsOrSession?: ResolutionOptionsOrSession, +): ResolutionOptions { + // backwards compatibility + if (optionsOrSession instanceof ResolutionSession) { + optionsOrSession = {session: optionsOrSession}; + } + return optionsOrSession || {}; +}